overtype 1.2.7 → 2.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/README.md +222 -34
- package/dist/overtype-webcomponent.esm.js +4763 -0
- package/dist/overtype-webcomponent.esm.js.map +7 -0
- package/dist/overtype-webcomponent.js +4785 -0
- package/dist/overtype-webcomponent.js.map +7 -0
- package/dist/overtype-webcomponent.min.js +991 -0
- package/dist/overtype.cjs +682 -389
- package/dist/overtype.cjs.map +4 -4
- package/dist/overtype.d.ts +57 -14
- package/dist/overtype.esm.js +679 -388
- package/dist/overtype.esm.js.map +4 -4
- package/dist/overtype.js +679 -388
- package/dist/overtype.js.map +4 -4
- package/dist/overtype.min.js +157 -125
- package/package.json +18 -4
- package/src/link-tooltip.js +48 -73
- package/src/overtype-webcomponent.js +676 -0
- package/src/overtype.d.ts +57 -14
- package/src/overtype.js +186 -59
- package/src/parser.js +120 -17
- package/src/styles.js +92 -30
- package/src/toolbar-buttons.js +163 -0
- package/src/toolbar.js +194 -249
- package/diagram.png +0 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OverType Web Component
|
|
3
|
+
* A custom element wrapper for the OverType markdown editor with Shadow DOM isolation
|
|
4
|
+
* @version 1.0.0
|
|
5
|
+
* @license MIT
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import OverType from './overtype.js';
|
|
9
|
+
import { generateStyles } from './styles.js';
|
|
10
|
+
import { getTheme } from './themes.js';
|
|
11
|
+
|
|
12
|
+
// Constants for better maintainability
|
|
13
|
+
const CONTAINER_CLASS = 'overtype-webcomponent-container';
|
|
14
|
+
const DEFAULT_PLACEHOLDER = 'Start typing...';
|
|
15
|
+
const OBSERVED_ATTRIBUTES = [
|
|
16
|
+
'value', 'theme', 'toolbar', 'height', 'min-height', 'max-height',
|
|
17
|
+
'placeholder', 'font-size', 'line-height', 'padding', 'auto-resize',
|
|
18
|
+
'autofocus', 'show-stats', 'smart-lists', 'readonly'
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* OverType Editor Web Component
|
|
23
|
+
* Provides a declarative API with complete style isolation via Shadow DOM
|
|
24
|
+
*/
|
|
25
|
+
class OverTypeEditor extends HTMLElement {
|
|
26
|
+
constructor() {
|
|
27
|
+
super();
|
|
28
|
+
|
|
29
|
+
// Create shadow root for style isolation
|
|
30
|
+
this.attachShadow({ mode: 'open' });
|
|
31
|
+
|
|
32
|
+
// Initialize instance variables
|
|
33
|
+
this._editor = null;
|
|
34
|
+
this._initialized = false;
|
|
35
|
+
this._pendingOptions = {};
|
|
36
|
+
this._styleVersion = 0;
|
|
37
|
+
this._baseStyleElement = null; // Track the component's base stylesheet
|
|
38
|
+
this._selectionChangeHandler = null; // Track selectionchange listener for cleanup
|
|
39
|
+
|
|
40
|
+
// Track initialization state
|
|
41
|
+
this._isConnected = false;
|
|
42
|
+
|
|
43
|
+
// Bind methods to maintain context
|
|
44
|
+
this._handleChange = this._handleChange.bind(this);
|
|
45
|
+
this._handleKeydown = this._handleKeydown.bind(this);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Decode common escape sequences from attribute string values
|
|
50
|
+
* @private
|
|
51
|
+
* @param {string|null|undefined} str
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
_decodeValue(str) {
|
|
55
|
+
if (typeof str !== 'string') return '';
|
|
56
|
+
// Replace common escape sequences (keep order: \\ first)
|
|
57
|
+
return str.replace(/\\r/g, '\r').replace(/\\n/g, '\n').replace(/\\t/g, '\t');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Note: _encodeValue removed as it's currently unused
|
|
61
|
+
// Can be re-added if needed for future attribute encoding
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Define observed attributes for reactive updates
|
|
65
|
+
*/
|
|
66
|
+
static get observedAttributes() {
|
|
67
|
+
return OBSERVED_ATTRIBUTES;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Component connected to DOM - initialize editor
|
|
72
|
+
*/
|
|
73
|
+
connectedCallback() {
|
|
74
|
+
this._isConnected = true;
|
|
75
|
+
this._initializeEditor();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Component disconnected from DOM - cleanup
|
|
80
|
+
*/
|
|
81
|
+
disconnectedCallback() {
|
|
82
|
+
this._isConnected = false;
|
|
83
|
+
this._cleanup();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Attribute changed callback - update editor options
|
|
88
|
+
*/
|
|
89
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
90
|
+
if (oldValue === newValue) return;
|
|
91
|
+
// Prevent recursive updates triggered by internal silent attribute sync
|
|
92
|
+
if (this._silentUpdate) return;
|
|
93
|
+
|
|
94
|
+
// Store pending changes if not initialized yet
|
|
95
|
+
if (!this._initialized) {
|
|
96
|
+
this._pendingOptions[name] = newValue;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Apply changes to existing editor
|
|
101
|
+
this._updateOption(name, newValue);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Initialize the OverType editor inside shadow DOM
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
_initializeEditor() {
|
|
109
|
+
if (this._initialized || !this._isConnected) return;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
// Create container inside shadow root
|
|
113
|
+
const container = document.createElement('div');
|
|
114
|
+
container.className = CONTAINER_CLASS;
|
|
115
|
+
|
|
116
|
+
// Set container height from attributes
|
|
117
|
+
const height = this.getAttribute('height');
|
|
118
|
+
const minHeight = this.getAttribute('min-height');
|
|
119
|
+
const maxHeight = this.getAttribute('max-height');
|
|
120
|
+
|
|
121
|
+
if (height) container.style.height = height;
|
|
122
|
+
if (minHeight) container.style.minHeight = minHeight;
|
|
123
|
+
if (maxHeight) container.style.maxHeight = maxHeight;
|
|
124
|
+
|
|
125
|
+
// Create and inject styles into shadow DOM
|
|
126
|
+
this._injectStyles();
|
|
127
|
+
|
|
128
|
+
// Append container to shadow root
|
|
129
|
+
this.shadowRoot.appendChild(container);
|
|
130
|
+
|
|
131
|
+
// Prepare OverType options from attributes
|
|
132
|
+
const options = this._getOptionsFromAttributes();
|
|
133
|
+
|
|
134
|
+
// Initialize OverType editor
|
|
135
|
+
const editorInstances = new OverType(container, options);
|
|
136
|
+
this._editor = editorInstances[0]; // OverType returns an array
|
|
137
|
+
|
|
138
|
+
this._initialized = true;
|
|
139
|
+
|
|
140
|
+
// Set up event listeners for Shadow DOM
|
|
141
|
+
// Global document listeners won't work in Shadow DOM, so we need local ones
|
|
142
|
+
if (this._editor && this._editor.textarea) {
|
|
143
|
+
// Scroll sync
|
|
144
|
+
this._editor.textarea.addEventListener('scroll', () => {
|
|
145
|
+
if (this._editor && this._editor.preview && this._editor.textarea) {
|
|
146
|
+
this._editor.preview.scrollTop = this._editor.textarea.scrollTop;
|
|
147
|
+
this._editor.preview.scrollLeft = this._editor.textarea.scrollLeft;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Input event for preview updates
|
|
152
|
+
this._editor.textarea.addEventListener('input', (e) => {
|
|
153
|
+
if (this._editor && this._editor.handleInput) {
|
|
154
|
+
this._editor.handleInput(e);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Keydown event for keyboard shortcuts and special key handling
|
|
159
|
+
this._editor.textarea.addEventListener('keydown', (e) => {
|
|
160
|
+
if (this._editor && this._editor.handleKeydown) {
|
|
161
|
+
this._editor.handleKeydown(e);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Selection change event for link tooltip and stats updates
|
|
166
|
+
// selectionchange only fires on document, so we need to check if the active element is inside our shadow root
|
|
167
|
+
this._selectionChangeHandler = () => {
|
|
168
|
+
// Check if this web component is the active element (focused)
|
|
169
|
+
if (document.activeElement === this) {
|
|
170
|
+
// The selection is inside our shadow root
|
|
171
|
+
const shadowActiveElement = this.shadowRoot.activeElement;
|
|
172
|
+
if (shadowActiveElement && shadowActiveElement === this._editor.textarea) {
|
|
173
|
+
// Update stats if enabled
|
|
174
|
+
if (this._editor.options.showStats && this._editor.statsBar) {
|
|
175
|
+
this._editor._updateStats();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Trigger link tooltip check
|
|
179
|
+
if (this._editor.linkTooltip && this._editor.linkTooltip.checkCursorPosition) {
|
|
180
|
+
this._editor.linkTooltip.checkCursorPosition();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
document.addEventListener('selectionchange', this._selectionChangeHandler);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Apply any pending option changes
|
|
189
|
+
this._applyPendingOptions();
|
|
190
|
+
|
|
191
|
+
// Dispatch ready event
|
|
192
|
+
this._dispatchEvent('ready', { editor: this._editor });
|
|
193
|
+
} catch (error) {
|
|
194
|
+
const message = error && error.message ? error.message : String(error);
|
|
195
|
+
// Avoid passing the raw Error object to console in jsdom to prevent recursive inspect issues
|
|
196
|
+
console.warn('OverType Web Component initialization failed:', message);
|
|
197
|
+
this._dispatchEvent('error', { error: { message } });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Inject styles into shadow DOM for complete isolation
|
|
203
|
+
* @private
|
|
204
|
+
*/
|
|
205
|
+
_injectStyles() {
|
|
206
|
+
const style = document.createElement('style');
|
|
207
|
+
|
|
208
|
+
// Get theme for style generation
|
|
209
|
+
const themeAttr = this.getAttribute('theme') || 'solar';
|
|
210
|
+
const theme = getTheme(themeAttr);
|
|
211
|
+
|
|
212
|
+
// Generate styles with current options
|
|
213
|
+
const options = this._getOptionsFromAttributes();
|
|
214
|
+
const styles = generateStyles({ ...options, theme });
|
|
215
|
+
|
|
216
|
+
// Add web component specific styles
|
|
217
|
+
const webComponentStyles = `
|
|
218
|
+
/* Web Component Host Styles */
|
|
219
|
+
:host {
|
|
220
|
+
display: block;
|
|
221
|
+
position: relative;
|
|
222
|
+
width: 100%;
|
|
223
|
+
height: 100%;
|
|
224
|
+
contain: layout style;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.overtype-webcomponent-container {
|
|
228
|
+
width: 100%;
|
|
229
|
+
height: 100%;
|
|
230
|
+
position: relative;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* Override container grid layout for web component */
|
|
234
|
+
.overtype-container {
|
|
235
|
+
height: 100% !important;
|
|
236
|
+
}
|
|
237
|
+
`;
|
|
238
|
+
|
|
239
|
+
this._styleVersion += 1;
|
|
240
|
+
const versionBanner = `\n/* overtype-webcomponent styles v${this._styleVersion} */\n`;
|
|
241
|
+
style.textContent = versionBanner + styles + webComponentStyles;
|
|
242
|
+
|
|
243
|
+
// Store reference to this base stylesheet so we can remove it specifically later
|
|
244
|
+
this._baseStyleElement = style;
|
|
245
|
+
this.shadowRoot.appendChild(style);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Extract options from HTML attributes
|
|
250
|
+
* @private
|
|
251
|
+
* @returns {Object} OverType options object
|
|
252
|
+
*/
|
|
253
|
+
_getOptionsFromAttributes() {
|
|
254
|
+
const options = {
|
|
255
|
+
// Allow authoring multi-line content via escaped sequences in attributes
|
|
256
|
+
// and fall back to light DOM text content if attribute is absent
|
|
257
|
+
value: this.getAttribute('value') !== null ? this._decodeValue(this.getAttribute('value')) : (this.textContent || '').trim(),
|
|
258
|
+
placeholder: this.getAttribute('placeholder') || DEFAULT_PLACEHOLDER,
|
|
259
|
+
toolbar: this.hasAttribute('toolbar'),
|
|
260
|
+
autofocus: this.hasAttribute('autofocus'),
|
|
261
|
+
autoResize: this.hasAttribute('auto-resize'),
|
|
262
|
+
showStats: this.hasAttribute('show-stats'),
|
|
263
|
+
smartLists: !this.hasAttribute('smart-lists') || this.getAttribute('smart-lists') !== 'false',
|
|
264
|
+
onChange: this._handleChange,
|
|
265
|
+
onKeydown: this._handleKeydown
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// Font and layout options
|
|
269
|
+
const fontSize = this.getAttribute('font-size');
|
|
270
|
+
if (fontSize) options.fontSize = fontSize;
|
|
271
|
+
|
|
272
|
+
const lineHeight = this.getAttribute('line-height');
|
|
273
|
+
if (lineHeight) options.lineHeight = parseFloat(lineHeight) || 1.6;
|
|
274
|
+
|
|
275
|
+
const padding = this.getAttribute('padding');
|
|
276
|
+
if (padding) options.padding = padding;
|
|
277
|
+
|
|
278
|
+
const minHeight = this.getAttribute('min-height');
|
|
279
|
+
if (minHeight) options.minHeight = minHeight;
|
|
280
|
+
|
|
281
|
+
const maxHeight = this.getAttribute('max-height');
|
|
282
|
+
if (maxHeight) options.maxHeight = maxHeight;
|
|
283
|
+
|
|
284
|
+
return options;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Apply pending option changes after initialization
|
|
289
|
+
* @private
|
|
290
|
+
*/
|
|
291
|
+
_applyPendingOptions() {
|
|
292
|
+
for (const [attr, value] of Object.entries(this._pendingOptions)) {
|
|
293
|
+
this._updateOption(attr, value);
|
|
294
|
+
}
|
|
295
|
+
this._pendingOptions = {};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Update a single editor option
|
|
300
|
+
* @private
|
|
301
|
+
* @param {string} attribute - Attribute name
|
|
302
|
+
* @param {string} value - New value
|
|
303
|
+
*/
|
|
304
|
+
_updateOption(attribute, value) {
|
|
305
|
+
if (!this._editor) return;
|
|
306
|
+
|
|
307
|
+
switch (attribute) {
|
|
308
|
+
case 'value':
|
|
309
|
+
{
|
|
310
|
+
const decoded = this._decodeValue(value);
|
|
311
|
+
if (this._editor.getValue() !== decoded) {
|
|
312
|
+
this._editor.setValue(decoded || '');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
break;
|
|
316
|
+
|
|
317
|
+
case 'theme':
|
|
318
|
+
// Theme changes require re-injecting styles
|
|
319
|
+
this._reinjectStyles();
|
|
320
|
+
break;
|
|
321
|
+
|
|
322
|
+
case 'placeholder':
|
|
323
|
+
if (this._editor.textarea) {
|
|
324
|
+
this._editor.textarea.placeholder = value || '';
|
|
325
|
+
}
|
|
326
|
+
break;
|
|
327
|
+
|
|
328
|
+
case 'readonly':
|
|
329
|
+
if (this._editor.textarea) {
|
|
330
|
+
this._editor.textarea.readOnly = this.hasAttribute('readonly');
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
|
|
334
|
+
case 'height':
|
|
335
|
+
case 'min-height':
|
|
336
|
+
case 'max-height':
|
|
337
|
+
this._updateContainerHeight();
|
|
338
|
+
break;
|
|
339
|
+
|
|
340
|
+
// For other options that require reinitialization
|
|
341
|
+
case 'toolbar':
|
|
342
|
+
// Only reinitialize if value actually changes
|
|
343
|
+
if (!!this.hasAttribute('toolbar') === !!this._editor.options.toolbar) return;
|
|
344
|
+
this._reinitializeEditor();
|
|
345
|
+
break;
|
|
346
|
+
case 'auto-resize':
|
|
347
|
+
if (!!this.hasAttribute('auto-resize') === !!this._editor.options.autoResize) return;
|
|
348
|
+
this._reinitializeEditor();
|
|
349
|
+
break;
|
|
350
|
+
case 'show-stats':
|
|
351
|
+
if (!!this.hasAttribute('show-stats') === !!this._editor.options.showStats) return;
|
|
352
|
+
this._reinitializeEditor();
|
|
353
|
+
break;
|
|
354
|
+
|
|
355
|
+
// Typography/layout style changes
|
|
356
|
+
case 'font-size': {
|
|
357
|
+
if (this._updateFontSize(value)) {
|
|
358
|
+
// Only reinject styles once if direct update succeeded
|
|
359
|
+
this._reinjectStyles();
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
case 'line-height': {
|
|
364
|
+
if (this._updateLineHeight(value)) {
|
|
365
|
+
// Only reinject styles once if direct update succeeded
|
|
366
|
+
this._reinjectStyles();
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
case 'padding':
|
|
371
|
+
this._reinjectStyles();
|
|
372
|
+
break;
|
|
373
|
+
|
|
374
|
+
// Smart-lists affects editing behavior → requires reinitialization
|
|
375
|
+
case 'smart-lists': {
|
|
376
|
+
const newSmartLists = !this.hasAttribute('smart-lists') || this.getAttribute('smart-lists') !== 'false';
|
|
377
|
+
if (!!this._editor.options.smartLists === !!newSmartLists) return;
|
|
378
|
+
this._reinitializeEditor();
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Update container height from attributes
|
|
386
|
+
* @private
|
|
387
|
+
*/
|
|
388
|
+
_updateContainerHeight() {
|
|
389
|
+
const container = this.shadowRoot.querySelector(`.${CONTAINER_CLASS}`);
|
|
390
|
+
if (!container) return;
|
|
391
|
+
|
|
392
|
+
const height = this.getAttribute('height');
|
|
393
|
+
const minHeight = this.getAttribute('min-height');
|
|
394
|
+
const maxHeight = this.getAttribute('max-height');
|
|
395
|
+
|
|
396
|
+
container.style.height = height || '';
|
|
397
|
+
container.style.minHeight = minHeight || '';
|
|
398
|
+
container.style.maxHeight = maxHeight || '';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Update font size efficiently
|
|
403
|
+
* @private
|
|
404
|
+
* @param {string} value - New font size value
|
|
405
|
+
* @returns {boolean} True if direct update succeeded
|
|
406
|
+
*/
|
|
407
|
+
_updateFontSize(value) {
|
|
408
|
+
if (this._editor && this._editor.wrapper) {
|
|
409
|
+
this._editor.options.fontSize = value || '';
|
|
410
|
+
this._editor.wrapper.style.setProperty('--instance-font-size', this._editor.options.fontSize);
|
|
411
|
+
this._editor.updatePreview();
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Update line height efficiently
|
|
419
|
+
* @private
|
|
420
|
+
* @param {string} value - New line height value
|
|
421
|
+
* @returns {boolean} True if direct update succeeded
|
|
422
|
+
*/
|
|
423
|
+
_updateLineHeight(value) {
|
|
424
|
+
if (this._editor && this._editor.wrapper) {
|
|
425
|
+
const numeric = parseFloat(value);
|
|
426
|
+
const lineHeight = Number.isFinite(numeric) ? numeric : this._editor.options.lineHeight;
|
|
427
|
+
this._editor.options.lineHeight = lineHeight;
|
|
428
|
+
this._editor.wrapper.style.setProperty('--instance-line-height', String(lineHeight));
|
|
429
|
+
this._editor.updatePreview();
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Re-inject styles (useful for theme changes)
|
|
437
|
+
* @private
|
|
438
|
+
*/
|
|
439
|
+
_reinjectStyles() {
|
|
440
|
+
// Remove only the base stylesheet, not other style elements (e.g., tooltip styles)
|
|
441
|
+
if (this._baseStyleElement && this._baseStyleElement.parentNode) {
|
|
442
|
+
this._baseStyleElement.remove();
|
|
443
|
+
}
|
|
444
|
+
this._injectStyles();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Reinitialize the entire editor (for major option changes)
|
|
449
|
+
* @private
|
|
450
|
+
*/
|
|
451
|
+
_reinitializeEditor() {
|
|
452
|
+
const currentValue = this._editor ? this._editor.getValue() : '';
|
|
453
|
+
this._cleanup();
|
|
454
|
+
this._initialized = false;
|
|
455
|
+
|
|
456
|
+
// Clear shadow root
|
|
457
|
+
this.shadowRoot.innerHTML = '';
|
|
458
|
+
|
|
459
|
+
// Preserve current value
|
|
460
|
+
if (currentValue && !this.getAttribute('value')) {
|
|
461
|
+
this.setAttribute('value', currentValue);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Reinitialize
|
|
465
|
+
this._initializeEditor();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Handle content changes from OverType
|
|
470
|
+
* @private
|
|
471
|
+
* @param {string} value - New editor value
|
|
472
|
+
*/
|
|
473
|
+
_handleChange(value) {
|
|
474
|
+
// Update value attribute without triggering attribute change
|
|
475
|
+
this._updateValueAttribute(value);
|
|
476
|
+
|
|
477
|
+
// Avoid dispatching change before initialization completes
|
|
478
|
+
if (!this._initialized || !this._editor) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Dispatch change event
|
|
483
|
+
this._dispatchEvent('change', {
|
|
484
|
+
value,
|
|
485
|
+
editor: this._editor
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Handle keydown events from OverType
|
|
491
|
+
* @private
|
|
492
|
+
* @param {KeyboardEvent} event - Keyboard event
|
|
493
|
+
*/
|
|
494
|
+
_handleKeydown(event) {
|
|
495
|
+
this._dispatchEvent('keydown', {
|
|
496
|
+
event,
|
|
497
|
+
editor: this._editor
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Update value attribute without triggering observer
|
|
503
|
+
* @private
|
|
504
|
+
* @param {string} value - New value
|
|
505
|
+
*/
|
|
506
|
+
_updateValueAttribute(value) {
|
|
507
|
+
// Temporarily store the current value to avoid infinite loop
|
|
508
|
+
const currentAttrValue = this.getAttribute('value');
|
|
509
|
+
if (currentAttrValue !== value) {
|
|
510
|
+
// Use a flag to prevent triggering the attribute observer
|
|
511
|
+
this._silentUpdate = true;
|
|
512
|
+
this.setAttribute('value', value);
|
|
513
|
+
this._silentUpdate = false;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Dispatch custom events
|
|
519
|
+
* @private
|
|
520
|
+
* @param {string} eventName - Event name
|
|
521
|
+
* @param {Object} detail - Event detail
|
|
522
|
+
*/
|
|
523
|
+
_dispatchEvent(eventName, detail = {}) {
|
|
524
|
+
const event = new CustomEvent(eventName, {
|
|
525
|
+
detail,
|
|
526
|
+
bubbles: true,
|
|
527
|
+
composed: true
|
|
528
|
+
});
|
|
529
|
+
this.dispatchEvent(event);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Cleanup editor and remove listeners
|
|
534
|
+
* @private
|
|
535
|
+
*/
|
|
536
|
+
_cleanup() {
|
|
537
|
+
// Remove selectionchange listener
|
|
538
|
+
if (this._selectionChangeHandler) {
|
|
539
|
+
document.removeEventListener('selectionchange', this._selectionChangeHandler);
|
|
540
|
+
this._selectionChangeHandler = null;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (this._editor && typeof this._editor.destroy === 'function') {
|
|
544
|
+
this._editor.destroy();
|
|
545
|
+
}
|
|
546
|
+
this._editor = null;
|
|
547
|
+
this._initialized = false;
|
|
548
|
+
|
|
549
|
+
// Clear shadow root to prevent stale containers on remount
|
|
550
|
+
// This is critical for React/Vue/etc. that frequently mount/unmount components
|
|
551
|
+
if (this.shadowRoot) {
|
|
552
|
+
this.shadowRoot.innerHTML = '';
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ===== PUBLIC API METHODS =====
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Refresh theme styles (useful when theme object is updated without changing theme name)
|
|
560
|
+
* @public
|
|
561
|
+
*/
|
|
562
|
+
refreshTheme() {
|
|
563
|
+
if (this._initialized) {
|
|
564
|
+
this._reinjectStyles();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Get current editor value
|
|
570
|
+
* @returns {string} Current markdown content
|
|
571
|
+
*/
|
|
572
|
+
getValue() {
|
|
573
|
+
return this._editor ? this._editor.getValue() : this.getAttribute('value') || '';
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Set editor value
|
|
578
|
+
* @param {string} value - New markdown content
|
|
579
|
+
*/
|
|
580
|
+
setValue(value) {
|
|
581
|
+
if (this._editor) {
|
|
582
|
+
this._editor.setValue(value);
|
|
583
|
+
} else {
|
|
584
|
+
this.setAttribute('value', value);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Get rendered HTML
|
|
590
|
+
* @returns {string} Rendered HTML
|
|
591
|
+
*/
|
|
592
|
+
getHTML() {
|
|
593
|
+
// Bridge to core editor API (getRenderedHTML)
|
|
594
|
+
return this._editor ? this._editor.getRenderedHTML(false) : '';
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Insert text at cursor position
|
|
599
|
+
* @param {string} text - Text to insert
|
|
600
|
+
*/
|
|
601
|
+
insertText(text) {
|
|
602
|
+
if (!this._editor || typeof text !== 'string') {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
this._editor.insertText(text);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Focus the editor
|
|
610
|
+
*/
|
|
611
|
+
focus() {
|
|
612
|
+
if (this._editor && this._editor.textarea) {
|
|
613
|
+
this._editor.textarea.focus();
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Blur the editor
|
|
619
|
+
*/
|
|
620
|
+
blur() {
|
|
621
|
+
if (this._editor && this._editor.textarea) {
|
|
622
|
+
this._editor.textarea.blur();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Get editor statistics
|
|
628
|
+
* @returns {Object} Statistics object
|
|
629
|
+
*/
|
|
630
|
+
getStats() {
|
|
631
|
+
if (!this._editor || !this._editor.textarea) return null;
|
|
632
|
+
|
|
633
|
+
const value = this._editor.textarea.value;
|
|
634
|
+
const lines = value.split('\n');
|
|
635
|
+
const chars = value.length;
|
|
636
|
+
const words = value.split(/\s+/).filter(w => w.length > 0).length;
|
|
637
|
+
|
|
638
|
+
// Calculate line and column from cursor position
|
|
639
|
+
const selectionStart = this._editor.textarea.selectionStart;
|
|
640
|
+
const beforeCursor = value.substring(0, selectionStart);
|
|
641
|
+
const linesBefore = beforeCursor.split('\n');
|
|
642
|
+
const currentLine = linesBefore.length;
|
|
643
|
+
const currentColumn = linesBefore[linesBefore.length - 1].length + 1;
|
|
644
|
+
|
|
645
|
+
return {
|
|
646
|
+
characters: chars,
|
|
647
|
+
words: words,
|
|
648
|
+
lines: lines.length,
|
|
649
|
+
line: currentLine,
|
|
650
|
+
column: currentColumn
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Check if editor is ready
|
|
656
|
+
* @returns {boolean} True if editor is initialized
|
|
657
|
+
*/
|
|
658
|
+
isReady() {
|
|
659
|
+
return this._initialized && this._editor !== null;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Get the internal OverType instance
|
|
664
|
+
* @returns {OverType} The OverType editor instance
|
|
665
|
+
*/
|
|
666
|
+
getEditor() {
|
|
667
|
+
return this._editor;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Register the custom element
|
|
672
|
+
if (!customElements.get('overtype-editor')) {
|
|
673
|
+
customElements.define('overtype-editor', OverTypeEditor);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
export default OverTypeEditor;
|