overtype 1.0.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/LICENSE +21 -0
- package/README.md +441 -0
- package/dist/overtype.esm.js +2576 -0
- package/dist/overtype.esm.js.map +7 -0
- package/dist/overtype.js +2599 -0
- package/dist/overtype.js.map +7 -0
- package/dist/overtype.min.js +546 -0
- package/package.json +50 -0
- package/src/icons.js +77 -0
- package/src/index.js +4 -0
- package/src/overtype.js +781 -0
- package/src/parser.js +222 -0
- package/src/shortcuts.js +125 -0
- package/src/styles.js +486 -0
- package/src/themes.js +124 -0
- package/src/toolbar.js +221 -0
package/src/overtype.js
ADDED
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OverType - A lightweight markdown editor library with perfect WYSIWYG alignment
|
|
3
|
+
* @version 1.0.0
|
|
4
|
+
* @license MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { MarkdownParser } from './parser.js';
|
|
8
|
+
import { ShortcutsManager } from './shortcuts.js';
|
|
9
|
+
import { generateStyles } from './styles.js';
|
|
10
|
+
import { getTheme, mergeTheme, solar } from './themes.js';
|
|
11
|
+
import { Toolbar } from './toolbar.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* OverType Editor Class
|
|
15
|
+
*/
|
|
16
|
+
class OverType {
|
|
17
|
+
// Static properties
|
|
18
|
+
static instances = new WeakMap();
|
|
19
|
+
static stylesInjected = false;
|
|
20
|
+
static globalListenersInitialized = false;
|
|
21
|
+
static instanceCount = 0;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Constructor - Always returns an array of instances
|
|
25
|
+
* @param {string|Element|NodeList|Array} target - Target element(s)
|
|
26
|
+
* @param {Object} options - Configuration options
|
|
27
|
+
* @returns {Array} Array of OverType instances
|
|
28
|
+
*/
|
|
29
|
+
constructor(target, options = {}) {
|
|
30
|
+
// Convert target to array of elements
|
|
31
|
+
let elements;
|
|
32
|
+
|
|
33
|
+
if (typeof target === 'string') {
|
|
34
|
+
elements = document.querySelectorAll(target);
|
|
35
|
+
if (elements.length === 0) {
|
|
36
|
+
throw new Error(`No elements found for selector: ${target}`);
|
|
37
|
+
}
|
|
38
|
+
elements = Array.from(elements);
|
|
39
|
+
} else if (target instanceof Element) {
|
|
40
|
+
elements = [target];
|
|
41
|
+
} else if (target instanceof NodeList) {
|
|
42
|
+
elements = Array.from(target);
|
|
43
|
+
} else if (Array.isArray(target)) {
|
|
44
|
+
elements = target;
|
|
45
|
+
} else {
|
|
46
|
+
throw new Error('Invalid target: must be selector string, Element, NodeList, or Array');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Initialize all elements and return array
|
|
50
|
+
const instances = elements.map(element => {
|
|
51
|
+
// Check for existing instance
|
|
52
|
+
if (element.overTypeInstance) {
|
|
53
|
+
// Re-init existing instance
|
|
54
|
+
element.overTypeInstance.reinit(options);
|
|
55
|
+
return element.overTypeInstance;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Create new instance
|
|
59
|
+
const instance = Object.create(OverType.prototype);
|
|
60
|
+
instance._init(element, options);
|
|
61
|
+
element.overTypeInstance = instance;
|
|
62
|
+
OverType.instances.set(element, instance);
|
|
63
|
+
return instance;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return instances;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Internal initialization
|
|
71
|
+
* @private
|
|
72
|
+
*/
|
|
73
|
+
_init(element, options = {}) {
|
|
74
|
+
this.element = element;
|
|
75
|
+
this.options = this._mergeOptions(options);
|
|
76
|
+
this.instanceId = ++OverType.instanceCount;
|
|
77
|
+
this.initialized = false;
|
|
78
|
+
|
|
79
|
+
// Inject styles if needed
|
|
80
|
+
OverType.injectStyles();
|
|
81
|
+
|
|
82
|
+
// Initialize global listeners
|
|
83
|
+
OverType.initGlobalListeners();
|
|
84
|
+
|
|
85
|
+
// Check for existing OverType DOM structure
|
|
86
|
+
const container = element.querySelector('.overtype-container');
|
|
87
|
+
const wrapper = element.querySelector('.overtype-wrapper');
|
|
88
|
+
if (container || wrapper) {
|
|
89
|
+
this._recoverFromDOM(container, wrapper);
|
|
90
|
+
} else {
|
|
91
|
+
this._buildFromScratch();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Setup shortcuts manager
|
|
95
|
+
this.shortcuts = new ShortcutsManager(this);
|
|
96
|
+
|
|
97
|
+
// Setup toolbar if enabled
|
|
98
|
+
if (this.options.toolbar) {
|
|
99
|
+
this.toolbar = new Toolbar(this);
|
|
100
|
+
this.toolbar.create();
|
|
101
|
+
|
|
102
|
+
// Update toolbar states on selection change
|
|
103
|
+
this.textarea.addEventListener('selectionchange', () => {
|
|
104
|
+
this.toolbar.updateButtonStates();
|
|
105
|
+
});
|
|
106
|
+
this.textarea.addEventListener('input', () => {
|
|
107
|
+
this.toolbar.updateButtonStates();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Mark as initialized
|
|
112
|
+
this.initialized = true;
|
|
113
|
+
|
|
114
|
+
// Call onChange if provided
|
|
115
|
+
if (this.options.onChange) {
|
|
116
|
+
this.options.onChange(this.getValue(), this);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Merge user options with defaults
|
|
122
|
+
* @private
|
|
123
|
+
*/
|
|
124
|
+
_mergeOptions(options) {
|
|
125
|
+
const defaults = {
|
|
126
|
+
// Typography
|
|
127
|
+
fontSize: '14px',
|
|
128
|
+
lineHeight: 1.6,
|
|
129
|
+
fontFamily: "'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace",
|
|
130
|
+
padding: '16px',
|
|
131
|
+
|
|
132
|
+
// Mobile styles
|
|
133
|
+
mobile: {
|
|
134
|
+
fontSize: '16px', // Prevent zoom on iOS
|
|
135
|
+
padding: '12px',
|
|
136
|
+
lineHeight: 1.5
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// Behavior
|
|
140
|
+
autofocus: false,
|
|
141
|
+
placeholder: 'Start typing...',
|
|
142
|
+
value: '',
|
|
143
|
+
|
|
144
|
+
// Callbacks
|
|
145
|
+
onChange: null,
|
|
146
|
+
onKeydown: null,
|
|
147
|
+
|
|
148
|
+
// Features
|
|
149
|
+
showActiveLineRaw: false,
|
|
150
|
+
showStats: false,
|
|
151
|
+
toolbar: false,
|
|
152
|
+
statsFormatter: null
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Remove theme and colors from options - these are now global
|
|
156
|
+
const { theme, colors, ...cleanOptions } = options;
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
...defaults,
|
|
160
|
+
...cleanOptions
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Recover from existing DOM structure
|
|
166
|
+
* @private
|
|
167
|
+
*/
|
|
168
|
+
_recoverFromDOM(container, wrapper) {
|
|
169
|
+
// Handle old structure (wrapper only) or new structure (container + wrapper)
|
|
170
|
+
if (container && container.classList.contains('overtype-container')) {
|
|
171
|
+
this.container = container;
|
|
172
|
+
this.wrapper = container.querySelector('.overtype-wrapper');
|
|
173
|
+
} else if (wrapper) {
|
|
174
|
+
// Old structure - just wrapper, no container
|
|
175
|
+
this.wrapper = wrapper;
|
|
176
|
+
// Wrap it in a container for consistency
|
|
177
|
+
this.container = document.createElement('div');
|
|
178
|
+
this.container.className = 'overtype-container';
|
|
179
|
+
const currentTheme = OverType.currentTheme || solar;
|
|
180
|
+
const themeName = typeof currentTheme === 'string' ? currentTheme : currentTheme.name;
|
|
181
|
+
if (themeName) {
|
|
182
|
+
this.container.setAttribute('data-theme', themeName);
|
|
183
|
+
}
|
|
184
|
+
wrapper.parentNode.insertBefore(this.container, wrapper);
|
|
185
|
+
this.container.appendChild(wrapper);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!this.wrapper) {
|
|
189
|
+
// No valid structure found
|
|
190
|
+
if (container) container.remove();
|
|
191
|
+
if (wrapper) wrapper.remove();
|
|
192
|
+
this._buildFromScratch();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
this.textarea = this.wrapper.querySelector('.overtype-input');
|
|
197
|
+
this.preview = this.wrapper.querySelector('.overtype-preview');
|
|
198
|
+
|
|
199
|
+
if (!this.textarea || !this.preview) {
|
|
200
|
+
// Partial DOM - clear and rebuild
|
|
201
|
+
this.container.remove();
|
|
202
|
+
this._buildFromScratch();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Store reference on wrapper
|
|
207
|
+
this.wrapper._instance = this;
|
|
208
|
+
|
|
209
|
+
// Apply instance-specific styles via CSS custom properties
|
|
210
|
+
if (this.options.fontSize) {
|
|
211
|
+
this.wrapper.style.setProperty('--instance-font-size', this.options.fontSize);
|
|
212
|
+
}
|
|
213
|
+
if (this.options.lineHeight) {
|
|
214
|
+
this.wrapper.style.setProperty('--instance-line-height', String(this.options.lineHeight));
|
|
215
|
+
}
|
|
216
|
+
if (this.options.padding) {
|
|
217
|
+
this.wrapper.style.setProperty('--instance-padding', this.options.padding);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Disable autofill, spellcheck, and extensions
|
|
221
|
+
this._configureTextarea();
|
|
222
|
+
|
|
223
|
+
// Apply any new options
|
|
224
|
+
this._applyOptions();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Build editor from scratch
|
|
229
|
+
* @private
|
|
230
|
+
*/
|
|
231
|
+
_buildFromScratch() {
|
|
232
|
+
// Extract any existing content
|
|
233
|
+
const content = this._extractContent();
|
|
234
|
+
|
|
235
|
+
// Clear element
|
|
236
|
+
this.element.innerHTML = '';
|
|
237
|
+
|
|
238
|
+
// Create DOM structure
|
|
239
|
+
this._createDOM();
|
|
240
|
+
|
|
241
|
+
// Set initial content
|
|
242
|
+
if (content || this.options.value) {
|
|
243
|
+
this.setValue(content || this.options.value);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Apply options
|
|
247
|
+
this._applyOptions();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Extract content from element
|
|
252
|
+
* @private
|
|
253
|
+
*/
|
|
254
|
+
_extractContent() {
|
|
255
|
+
// Look for existing OverType textarea
|
|
256
|
+
const textarea = this.element.querySelector('.overtype-input');
|
|
257
|
+
if (textarea) return textarea.value;
|
|
258
|
+
|
|
259
|
+
// Use element's text content as fallback
|
|
260
|
+
return this.element.textContent || '';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Create DOM structure
|
|
265
|
+
* @private
|
|
266
|
+
*/
|
|
267
|
+
_createDOM() {
|
|
268
|
+
// Create container that will hold toolbar and editor
|
|
269
|
+
this.container = document.createElement('div');
|
|
270
|
+
this.container.className = 'overtype-container';
|
|
271
|
+
|
|
272
|
+
// Set current global theme on container
|
|
273
|
+
const currentTheme = OverType.currentTheme || solar;
|
|
274
|
+
const themeName = typeof currentTheme === 'string' ? currentTheme : currentTheme.name;
|
|
275
|
+
if (themeName) {
|
|
276
|
+
this.container.setAttribute('data-theme', themeName);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Create wrapper for editor
|
|
280
|
+
this.wrapper = document.createElement('div');
|
|
281
|
+
this.wrapper.className = 'overtype-wrapper';
|
|
282
|
+
|
|
283
|
+
// Add stats wrapper class if stats are enabled
|
|
284
|
+
if (this.options.showStats) {
|
|
285
|
+
this.wrapper.classList.add('with-stats');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Apply instance-specific styles via CSS custom properties
|
|
289
|
+
if (this.options.fontSize) {
|
|
290
|
+
this.wrapper.style.setProperty('--instance-font-size', this.options.fontSize);
|
|
291
|
+
}
|
|
292
|
+
if (this.options.lineHeight) {
|
|
293
|
+
this.wrapper.style.setProperty('--instance-line-height', String(this.options.lineHeight));
|
|
294
|
+
}
|
|
295
|
+
if (this.options.padding) {
|
|
296
|
+
this.wrapper.style.setProperty('--instance-padding', this.options.padding);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
this.wrapper._instance = this;
|
|
300
|
+
|
|
301
|
+
// Create textarea
|
|
302
|
+
this.textarea = document.createElement('textarea');
|
|
303
|
+
this.textarea.className = 'overtype-input';
|
|
304
|
+
this.textarea.placeholder = this.options.placeholder;
|
|
305
|
+
this._configureTextarea();
|
|
306
|
+
|
|
307
|
+
// Create preview div
|
|
308
|
+
this.preview = document.createElement('div');
|
|
309
|
+
this.preview.className = 'overtype-preview';
|
|
310
|
+
this.preview.setAttribute('aria-hidden', 'true');
|
|
311
|
+
|
|
312
|
+
// Assemble DOM
|
|
313
|
+
this.wrapper.appendChild(this.textarea);
|
|
314
|
+
this.wrapper.appendChild(this.preview);
|
|
315
|
+
|
|
316
|
+
// Add stats bar if enabled
|
|
317
|
+
if (this.options.showStats) {
|
|
318
|
+
this.statsBar = document.createElement('div');
|
|
319
|
+
this.statsBar.className = 'overtype-stats';
|
|
320
|
+
this.wrapper.appendChild(this.statsBar);
|
|
321
|
+
this._updateStats();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Add wrapper to container
|
|
325
|
+
this.container.appendChild(this.wrapper);
|
|
326
|
+
|
|
327
|
+
// Add container to element
|
|
328
|
+
this.element.appendChild(this.container);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Configure textarea attributes
|
|
333
|
+
* @private
|
|
334
|
+
*/
|
|
335
|
+
_configureTextarea() {
|
|
336
|
+
this.textarea.setAttribute('autocomplete', 'off');
|
|
337
|
+
this.textarea.setAttribute('autocorrect', 'off');
|
|
338
|
+
this.textarea.setAttribute('autocapitalize', 'off');
|
|
339
|
+
this.textarea.setAttribute('spellcheck', 'false');
|
|
340
|
+
this.textarea.setAttribute('data-gramm', 'false');
|
|
341
|
+
this.textarea.setAttribute('data-gramm_editor', 'false');
|
|
342
|
+
this.textarea.setAttribute('data-enable-grammarly', 'false');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Apply options to the editor
|
|
347
|
+
* @private
|
|
348
|
+
*/
|
|
349
|
+
_applyOptions() {
|
|
350
|
+
// Apply autofocus
|
|
351
|
+
if (this.options.autofocus) {
|
|
352
|
+
this.textarea.focus();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Update preview with initial content
|
|
356
|
+
this.updatePreview();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Update preview with parsed markdown
|
|
361
|
+
*/
|
|
362
|
+
updatePreview() {
|
|
363
|
+
const text = this.textarea.value;
|
|
364
|
+
const cursorPos = this.textarea.selectionStart;
|
|
365
|
+
const activeLine = this._getCurrentLine(text, cursorPos);
|
|
366
|
+
|
|
367
|
+
// Parse markdown
|
|
368
|
+
const html = MarkdownParser.parse(text, activeLine, this.options.showActiveLineRaw);
|
|
369
|
+
this.preview.innerHTML = html || '<span style="color: #808080;">Start typing...</span>';
|
|
370
|
+
|
|
371
|
+
// Apply code block backgrounds
|
|
372
|
+
this._applyCodeBlockBackgrounds();
|
|
373
|
+
|
|
374
|
+
// Update stats if enabled
|
|
375
|
+
if (this.options.showStats && this.statsBar) {
|
|
376
|
+
this._updateStats();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Trigger onChange callback
|
|
380
|
+
if (this.options.onChange && this.initialized) {
|
|
381
|
+
this.options.onChange(text, this);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Apply background styling to code blocks
|
|
387
|
+
* @private
|
|
388
|
+
*/
|
|
389
|
+
_applyCodeBlockBackgrounds() {
|
|
390
|
+
// Find all code fence elements
|
|
391
|
+
const codeFences = this.preview.querySelectorAll('.code-fence');
|
|
392
|
+
|
|
393
|
+
// Process pairs of code fences
|
|
394
|
+
for (let i = 0; i < codeFences.length - 1; i += 2) {
|
|
395
|
+
const openFence = codeFences[i];
|
|
396
|
+
const closeFence = codeFences[i + 1];
|
|
397
|
+
|
|
398
|
+
// Get parent divs
|
|
399
|
+
const openParent = openFence.parentElement;
|
|
400
|
+
const closeParent = closeFence.parentElement;
|
|
401
|
+
|
|
402
|
+
if (!openParent || !closeParent) continue;
|
|
403
|
+
|
|
404
|
+
// Make fences display: block
|
|
405
|
+
openFence.style.display = 'block';
|
|
406
|
+
closeFence.style.display = 'block';
|
|
407
|
+
|
|
408
|
+
// Apply class to parent divs
|
|
409
|
+
openParent.classList.add('code-block-line');
|
|
410
|
+
closeParent.classList.add('code-block-line');
|
|
411
|
+
|
|
412
|
+
// Apply class to all divs between the parent divs
|
|
413
|
+
let currentDiv = openParent.nextElementSibling;
|
|
414
|
+
while (currentDiv && currentDiv !== closeParent) {
|
|
415
|
+
// Apply class to divs between the fences
|
|
416
|
+
if (currentDiv.tagName === 'DIV') {
|
|
417
|
+
currentDiv.classList.add('code-block-line');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Move to next sibling
|
|
421
|
+
currentDiv = currentDiv.nextElementSibling;
|
|
422
|
+
|
|
423
|
+
// Safety check to prevent infinite loop
|
|
424
|
+
if (!currentDiv) break;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Get current line number from cursor position
|
|
431
|
+
* @private
|
|
432
|
+
*/
|
|
433
|
+
_getCurrentLine(text, cursorPos) {
|
|
434
|
+
const lines = text.substring(0, cursorPos).split('\n');
|
|
435
|
+
return lines.length - 1;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Handle input events
|
|
440
|
+
* @private
|
|
441
|
+
*/
|
|
442
|
+
handleInput(event) {
|
|
443
|
+
this.updatePreview();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Handle keydown events
|
|
448
|
+
* @private
|
|
449
|
+
*/
|
|
450
|
+
handleKeydown(event) {
|
|
451
|
+
// Let shortcuts manager handle it first
|
|
452
|
+
const handled = this.shortcuts.handleKeydown(event);
|
|
453
|
+
|
|
454
|
+
// Call user callback if provided
|
|
455
|
+
if (!handled && this.options.onKeydown) {
|
|
456
|
+
this.options.onKeydown(event, this);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Handle scroll events
|
|
462
|
+
* @private
|
|
463
|
+
*/
|
|
464
|
+
handleScroll(event) {
|
|
465
|
+
// Sync preview scroll with textarea
|
|
466
|
+
this.preview.scrollTop = this.textarea.scrollTop;
|
|
467
|
+
this.preview.scrollLeft = this.textarea.scrollLeft;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Get editor content
|
|
472
|
+
* @returns {string} Current markdown content
|
|
473
|
+
*/
|
|
474
|
+
getValue() {
|
|
475
|
+
return this.textarea.value;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Set editor content
|
|
480
|
+
* @param {string} value - Markdown content to set
|
|
481
|
+
*/
|
|
482
|
+
setValue(value) {
|
|
483
|
+
this.textarea.value = value;
|
|
484
|
+
this.updatePreview();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Focus the editor
|
|
490
|
+
*/
|
|
491
|
+
focus() {
|
|
492
|
+
this.textarea.focus();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Blur the editor
|
|
497
|
+
*/
|
|
498
|
+
blur() {
|
|
499
|
+
this.textarea.blur();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Check if editor is initialized
|
|
504
|
+
* @returns {boolean}
|
|
505
|
+
*/
|
|
506
|
+
isInitialized() {
|
|
507
|
+
return this.initialized;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Re-initialize with new options
|
|
512
|
+
* @param {Object} options - New options to apply
|
|
513
|
+
*/
|
|
514
|
+
reinit(options = {}) {
|
|
515
|
+
this.options = this._mergeOptions({ ...this.options, ...options });
|
|
516
|
+
this._applyOptions();
|
|
517
|
+
this.updatePreview();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Update stats bar
|
|
522
|
+
* @private
|
|
523
|
+
*/
|
|
524
|
+
_updateStats() {
|
|
525
|
+
if (!this.statsBar) return;
|
|
526
|
+
|
|
527
|
+
const value = this.textarea.value;
|
|
528
|
+
const lines = value.split('\n');
|
|
529
|
+
const chars = value.length;
|
|
530
|
+
const words = value.split(/\s+/).filter(w => w.length > 0).length;
|
|
531
|
+
|
|
532
|
+
// Calculate line and column
|
|
533
|
+
const selectionStart = this.textarea.selectionStart;
|
|
534
|
+
const beforeCursor = value.substring(0, selectionStart);
|
|
535
|
+
const linesBeforeCursor = beforeCursor.split('\n');
|
|
536
|
+
const currentLine = linesBeforeCursor.length;
|
|
537
|
+
const currentColumn = linesBeforeCursor[linesBeforeCursor.length - 1].length + 1;
|
|
538
|
+
|
|
539
|
+
// Use custom formatter if provided
|
|
540
|
+
if (this.options.statsFormatter) {
|
|
541
|
+
this.statsBar.innerHTML = this.options.statsFormatter({
|
|
542
|
+
chars,
|
|
543
|
+
words,
|
|
544
|
+
lines: lines.length,
|
|
545
|
+
line: currentLine,
|
|
546
|
+
column: currentColumn
|
|
547
|
+
});
|
|
548
|
+
} else {
|
|
549
|
+
// Default format with live dot
|
|
550
|
+
this.statsBar.innerHTML = `
|
|
551
|
+
<div class="overtype-stat">
|
|
552
|
+
<span class="live-dot"></span>
|
|
553
|
+
<span>${chars} chars, ${words} words, ${lines.length} lines</span>
|
|
554
|
+
</div>
|
|
555
|
+
<div class="overtype-stat">Line ${currentLine}, Col ${currentColumn}</div>
|
|
556
|
+
`;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Show or hide stats bar
|
|
562
|
+
* @param {boolean} show - Whether to show stats
|
|
563
|
+
*/
|
|
564
|
+
showStats(show) {
|
|
565
|
+
this.options.showStats = show;
|
|
566
|
+
|
|
567
|
+
if (show && !this.statsBar) {
|
|
568
|
+
// Create stats bar
|
|
569
|
+
this.statsBar = document.createElement('div');
|
|
570
|
+
this.statsBar.className = 'overtype-stats';
|
|
571
|
+
this.wrapper.appendChild(this.statsBar);
|
|
572
|
+
this.wrapper.classList.add('with-stats');
|
|
573
|
+
this._updateStats();
|
|
574
|
+
} else if (!show && this.statsBar) {
|
|
575
|
+
// Remove stats bar
|
|
576
|
+
this.statsBar.remove();
|
|
577
|
+
this.statsBar = null;
|
|
578
|
+
this.wrapper.classList.remove('with-stats');
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Destroy the editor instance
|
|
584
|
+
*/
|
|
585
|
+
destroy() {
|
|
586
|
+
// Remove instance reference
|
|
587
|
+
this.element.overTypeInstance = null;
|
|
588
|
+
OverType.instances.delete(this.element);
|
|
589
|
+
|
|
590
|
+
// Cleanup shortcuts
|
|
591
|
+
if (this.shortcuts) {
|
|
592
|
+
this.shortcuts.destroy();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Remove DOM if created by us
|
|
596
|
+
if (this.wrapper) {
|
|
597
|
+
const content = this.getValue();
|
|
598
|
+
this.wrapper.remove();
|
|
599
|
+
|
|
600
|
+
// Restore original content
|
|
601
|
+
this.element.textContent = content;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
this.initialized = false;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ===== Static Methods =====
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Initialize multiple editors (static convenience method)
|
|
611
|
+
* @param {string|Element|NodeList|Array} target - Target element(s)
|
|
612
|
+
* @param {Object} options - Configuration options
|
|
613
|
+
* @returns {Array} Array of OverType instances
|
|
614
|
+
*/
|
|
615
|
+
static init(target, options = {}) {
|
|
616
|
+
return new OverType(target, options);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Get instance from element
|
|
621
|
+
* @param {Element} element - DOM element
|
|
622
|
+
* @returns {OverType|null} OverType instance or null
|
|
623
|
+
*/
|
|
624
|
+
static getInstance(element) {
|
|
625
|
+
return element.overTypeInstance || OverType.instances.get(element) || null;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Destroy all instances
|
|
630
|
+
*/
|
|
631
|
+
static destroyAll() {
|
|
632
|
+
const elements = document.querySelectorAll('[data-overtype-instance]');
|
|
633
|
+
elements.forEach(element => {
|
|
634
|
+
const instance = OverType.getInstance(element);
|
|
635
|
+
if (instance) {
|
|
636
|
+
instance.destroy();
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Inject styles into the document
|
|
643
|
+
* @param {boolean} force - Force re-injection
|
|
644
|
+
*/
|
|
645
|
+
static injectStyles(force = false) {
|
|
646
|
+
if (OverType.stylesInjected && !force) return;
|
|
647
|
+
|
|
648
|
+
// Remove any existing OverType styles
|
|
649
|
+
const existing = document.querySelector('style.overtype-styles');
|
|
650
|
+
if (existing) {
|
|
651
|
+
existing.remove();
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Generate and inject new styles with current theme
|
|
655
|
+
const theme = OverType.currentTheme || solar;
|
|
656
|
+
const styles = generateStyles({ theme });
|
|
657
|
+
const styleEl = document.createElement('style');
|
|
658
|
+
styleEl.className = 'overtype-styles';
|
|
659
|
+
styleEl.textContent = styles;
|
|
660
|
+
document.head.appendChild(styleEl);
|
|
661
|
+
|
|
662
|
+
OverType.stylesInjected = true;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Set global theme for all OverType instances
|
|
667
|
+
* @param {string|Object} theme - Theme name or custom theme object
|
|
668
|
+
* @param {Object} customColors - Optional color overrides
|
|
669
|
+
*/
|
|
670
|
+
static setTheme(theme, customColors = null) {
|
|
671
|
+
// Process theme
|
|
672
|
+
let themeObj = typeof theme === 'string' ? getTheme(theme) : theme;
|
|
673
|
+
|
|
674
|
+
// Apply custom colors if provided
|
|
675
|
+
if (customColors) {
|
|
676
|
+
themeObj = mergeTheme(themeObj, customColors);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Store as current theme
|
|
680
|
+
OverType.currentTheme = themeObj;
|
|
681
|
+
|
|
682
|
+
// Re-inject styles with new theme
|
|
683
|
+
OverType.injectStyles(true);
|
|
684
|
+
|
|
685
|
+
// Update all existing instances - update container theme attribute
|
|
686
|
+
document.querySelectorAll('.overtype-container').forEach(container => {
|
|
687
|
+
const themeName = typeof themeObj === 'string' ? themeObj : themeObj.name;
|
|
688
|
+
if (themeName) {
|
|
689
|
+
container.setAttribute('data-theme', themeName);
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Also handle any old-style wrappers without containers
|
|
694
|
+
document.querySelectorAll('.overtype-wrapper').forEach(wrapper => {
|
|
695
|
+
if (!wrapper.closest('.overtype-container')) {
|
|
696
|
+
const themeName = typeof themeObj === 'string' ? themeObj : themeObj.name;
|
|
697
|
+
if (themeName) {
|
|
698
|
+
wrapper.setAttribute('data-theme', themeName);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Trigger preview update for the instance
|
|
703
|
+
const instance = wrapper._instance;
|
|
704
|
+
if (instance) {
|
|
705
|
+
instance.updatePreview();
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Initialize global event listeners
|
|
712
|
+
*/
|
|
713
|
+
static initGlobalListeners() {
|
|
714
|
+
if (OverType.globalListenersInitialized) return;
|
|
715
|
+
|
|
716
|
+
// Input event
|
|
717
|
+
document.addEventListener('input', (e) => {
|
|
718
|
+
if (e.target && e.target.classList && e.target.classList.contains('overtype-input')) {
|
|
719
|
+
const wrapper = e.target.closest('.overtype-wrapper');
|
|
720
|
+
const instance = wrapper?._instance;
|
|
721
|
+
if (instance) instance.handleInput(e);
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
// Keydown event
|
|
726
|
+
document.addEventListener('keydown', (e) => {
|
|
727
|
+
if (e.target && e.target.classList && e.target.classList.contains('overtype-input')) {
|
|
728
|
+
const wrapper = e.target.closest('.overtype-wrapper');
|
|
729
|
+
const instance = wrapper?._instance;
|
|
730
|
+
if (instance) instance.handleKeydown(e);
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// Scroll event
|
|
735
|
+
document.addEventListener('scroll', (e) => {
|
|
736
|
+
if (e.target && e.target.classList && e.target.classList.contains('overtype-input')) {
|
|
737
|
+
const wrapper = e.target.closest('.overtype-wrapper');
|
|
738
|
+
const instance = wrapper?._instance;
|
|
739
|
+
if (instance) instance.handleScroll(e);
|
|
740
|
+
}
|
|
741
|
+
}, true);
|
|
742
|
+
|
|
743
|
+
// Selection change event
|
|
744
|
+
document.addEventListener('selectionchange', (e) => {
|
|
745
|
+
const activeElement = document.activeElement;
|
|
746
|
+
if (activeElement && activeElement.classList.contains('overtype-input')) {
|
|
747
|
+
const wrapper = activeElement.closest('.overtype-wrapper');
|
|
748
|
+
const instance = wrapper?._instance;
|
|
749
|
+
if (instance) {
|
|
750
|
+
// Update stats bar for cursor position
|
|
751
|
+
if (instance.options.showStats && instance.statsBar) {
|
|
752
|
+
instance._updateStats();
|
|
753
|
+
}
|
|
754
|
+
// Debounce updates
|
|
755
|
+
clearTimeout(instance._selectionTimeout);
|
|
756
|
+
instance._selectionTimeout = setTimeout(() => {
|
|
757
|
+
instance.updatePreview();
|
|
758
|
+
}, 50);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
OverType.globalListenersInitialized = true;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Export classes for advanced usage
|
|
768
|
+
OverType.MarkdownParser = MarkdownParser;
|
|
769
|
+
OverType.ShortcutsManager = ShortcutsManager;
|
|
770
|
+
|
|
771
|
+
// Export theme utilities
|
|
772
|
+
OverType.themes = { solar, cave: getTheme('cave') };
|
|
773
|
+
OverType.getTheme = getTheme;
|
|
774
|
+
|
|
775
|
+
// Set default theme
|
|
776
|
+
OverType.currentTheme = solar;
|
|
777
|
+
|
|
778
|
+
// For IIFE builds, esbuild needs the class as the default export
|
|
779
|
+
export default OverType;
|
|
780
|
+
// Also export as named for ESM compatibility
|
|
781
|
+
export { OverType };
|