Commit ·
16da498
1
Parent(s): afdf449
fix: Critical editor bugs - Dropdowns now visible (remove overflow:hidden from editor-shell) - Font/size/color menus z-index 200 (above editor) - Selection collapses after formatting (no more blue highlight) - Spelling overlay uses textContent for consistent offsets - Correction preserves formatting (replaces inside parent node) - Alt correction same fix - Fallback finds span by matching original text
Browse files- src/css/components.css +15 -9
- src/js/editor.js +50 -14
- src/js/format.js +17 -5
- src/js/selection.js +3 -1
src/css/components.css
CHANGED
|
@@ -244,8 +244,17 @@
|
|
| 244 |
background: var(--color-surface);
|
| 245 |
border: 1px solid var(--color-border);
|
| 246 |
border-radius: var(--radius-card);
|
| 247 |
-
overflow:
|
| 248 |
box-shadow: var(--shadow-card);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
}
|
| 250 |
|
| 251 |
.editor-toolbar {
|
|
@@ -292,13 +301,10 @@
|
|
| 292 |
padding: 8px var(--spacing-md);
|
| 293 |
border-bottom: 1px solid var(--color-border);
|
| 294 |
background: var(--color-surface);
|
| 295 |
-
overflow
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
.format-toolbar::-webkit-scrollbar {
|
| 301 |
-
display: none;
|
| 302 |
}
|
| 303 |
|
| 304 |
.fmt-group {
|
|
@@ -418,7 +424,7 @@
|
|
| 418 |
border-radius: 8px;
|
| 419 |
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
| 420 |
padding: 4px;
|
| 421 |
-
z-index:
|
| 422 |
opacity: 0;
|
| 423 |
visibility: hidden;
|
| 424 |
transform: translateY(-8px);
|
|
|
|
| 244 |
background: var(--color-surface);
|
| 245 |
border: 1px solid var(--color-border);
|
| 246 |
border-radius: var(--radius-card);
|
| 247 |
+
overflow: visible;
|
| 248 |
box-shadow: var(--shadow-card);
|
| 249 |
+
position: relative;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.editor-shell > *:first-child {
|
| 253 |
+
border-radius: var(--radius-card) var(--radius-card) 0 0;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.editor-shell > *:last-child {
|
| 257 |
+
border-radius: 0 0 var(--radius-card) var(--radius-card);
|
| 258 |
}
|
| 259 |
|
| 260 |
.editor-toolbar {
|
|
|
|
| 301 |
padding: 8px var(--spacing-md);
|
| 302 |
border-bottom: 1px solid var(--color-border);
|
| 303 |
background: var(--color-surface);
|
| 304 |
+
overflow: visible;
|
| 305 |
+
position: relative;
|
| 306 |
+
z-index: 50;
|
| 307 |
+
flex-shrink: 0;
|
|
|
|
|
|
|
|
|
|
| 308 |
}
|
| 309 |
|
| 310 |
.fmt-group {
|
|
|
|
| 424 |
border-radius: 8px;
|
| 425 |
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
| 426 |
padding: 4px;
|
| 427 |
+
z-index: 200;
|
| 428 |
opacity: 0;
|
| 429 |
visibility: hidden;
|
| 430 |
transform: translateY(-8px);
|
src/js/editor.js
CHANGED
|
@@ -307,16 +307,35 @@ function applySuggestionAtOffsets(suggestion) {
|
|
| 307 |
const errorSpan = idx >= 0 ? document.querySelector(`[data-suggestion-id="${idx}"]`) : null;
|
| 308 |
|
| 309 |
if (errorSpan) {
|
| 310 |
-
// Replace the error span with the
|
|
|
|
|
|
|
| 311 |
const correctedNode = document.createTextNode(suggestion.correction);
|
| 312 |
-
|
|
|
|
|
|
|
| 313 |
} else {
|
| 314 |
-
// Fallback:
|
| 315 |
-
const
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
}
|
| 321 |
hideTooltip();
|
| 322 |
analyzeTextDelayed();
|
|
@@ -332,14 +351,31 @@ function applyAlternativeCorrection(suggestion, correctionText) {
|
|
| 332 |
const errorSpan = idx >= 0 ? document.querySelector(`[data-suggestion-id="${idx}"]`) : null;
|
| 333 |
|
| 334 |
if (errorSpan) {
|
|
|
|
| 335 |
const correctedNode = document.createTextNode(correctionText);
|
| 336 |
-
|
|
|
|
|
|
|
| 337 |
} else {
|
| 338 |
-
const
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
}
|
| 344 |
hideTooltip();
|
| 345 |
analyzeTextDelayed();
|
|
|
|
| 307 |
const errorSpan = idx >= 0 ? document.querySelector(`[data-suggestion-id="${idx}"]`) : null;
|
| 308 |
|
| 309 |
if (errorSpan) {
|
| 310 |
+
// Replace the error span's text content with the correction
|
| 311 |
+
// while keeping it inside its formatting parent
|
| 312 |
+
const parent = errorSpan.parentNode;
|
| 313 |
const correctedNode = document.createTextNode(suggestion.correction);
|
| 314 |
+
parent.insertBefore(correctedNode, errorSpan);
|
| 315 |
+
parent.removeChild(errorSpan);
|
| 316 |
+
parent.normalize();
|
| 317 |
} else {
|
| 318 |
+
// Fallback: find span by matching original text
|
| 319 |
+
const allErrorSpans = document.querySelectorAll('.spelling-error, .grammar-error, .punctuation-suggestion');
|
| 320 |
+
let found = false;
|
| 321 |
+
allErrorSpans.forEach(span => {
|
| 322 |
+
if (!found && span.textContent === suggestion.original) {
|
| 323 |
+
const p = span.parentNode;
|
| 324 |
+
const correctedNode = document.createTextNode(suggestion.correction);
|
| 325 |
+
p.insertBefore(correctedNode, span);
|
| 326 |
+
p.removeChild(span);
|
| 327 |
+
p.normalize();
|
| 328 |
+
found = true;
|
| 329 |
+
}
|
| 330 |
+
});
|
| 331 |
+
if (!found) {
|
| 332 |
+
// Last resort: offset-based replacement
|
| 333 |
+
const text = getEditorText();
|
| 334 |
+
const before = text.substring(0, suggestion.start);
|
| 335 |
+
const after = text.substring(suggestion.end);
|
| 336 |
+
const newText = before + suggestion.correction + after;
|
| 337 |
+
setEditorHTML(escapeHtml(newText));
|
| 338 |
+
}
|
| 339 |
}
|
| 340 |
hideTooltip();
|
| 341 |
analyzeTextDelayed();
|
|
|
|
| 351 |
const errorSpan = idx >= 0 ? document.querySelector(`[data-suggestion-id="${idx}"]`) : null;
|
| 352 |
|
| 353 |
if (errorSpan) {
|
| 354 |
+
const parent = errorSpan.parentNode;
|
| 355 |
const correctedNode = document.createTextNode(correctionText);
|
| 356 |
+
parent.insertBefore(correctedNode, errorSpan);
|
| 357 |
+
parent.removeChild(errorSpan);
|
| 358 |
+
parent.normalize();
|
| 359 |
} else {
|
| 360 |
+
const allErrorSpans = document.querySelectorAll('.spelling-error, .grammar-error, .punctuation-suggestion');
|
| 361 |
+
let found = false;
|
| 362 |
+
allErrorSpans.forEach(span => {
|
| 363 |
+
if (!found && span.textContent === suggestion.original) {
|
| 364 |
+
const p = span.parentNode;
|
| 365 |
+
const correctedNode = document.createTextNode(correctionText);
|
| 366 |
+
p.insertBefore(correctedNode, span);
|
| 367 |
+
p.removeChild(span);
|
| 368 |
+
p.normalize();
|
| 369 |
+
found = true;
|
| 370 |
+
}
|
| 371 |
+
});
|
| 372 |
+
if (!found) {
|
| 373 |
+
const text = getEditorText();
|
| 374 |
+
const before = text.substring(0, suggestion.start);
|
| 375 |
+
const after = text.substring(suggestion.end);
|
| 376 |
+
const newText = before + correctionText + after;
|
| 377 |
+
setEditorHTML(escapeHtml(newText));
|
| 378 |
+
}
|
| 379 |
}
|
| 380 |
hideTooltip();
|
| 381 |
analyzeTextDelayed();
|
src/js/format.js
CHANGED
|
@@ -3,11 +3,23 @@
|
|
| 3 |
|
| 4 |
/**
|
| 5 |
* Execute a formatting command on the current selection
|
|
|
|
|
|
|
|
|
|
| 6 |
*/
|
| 7 |
-
function execFormat(command, value) {
|
| 8 |
-
document.execCommand(command, false, value
|
| 9 |
const editor = getEditorElement();
|
| 10 |
if (editor) editor.focus();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
updateFormatState();
|
| 12 |
}
|
| 13 |
|
|
@@ -18,10 +30,10 @@ function formatUnderline() { execFormat('underline'); }
|
|
| 18 |
function formatStrikethrough() { execFormat('strikethrough'); }
|
| 19 |
|
| 20 |
/* ── Undo / Redo (handles both typing and formatting) ── */
|
| 21 |
-
function formatUndo() { execFormat('undo'); }
|
| 22 |
-
function formatRedo() { execFormat('redo'); }
|
| 23 |
|
| 24 |
-
/* ── Alignment (applies to paragraph containing selection) ── */
|
| 25 |
function formatAlignRight() { execFormat('justifyRight'); }
|
| 26 |
function formatAlignCenter() { execFormat('justifyCenter'); }
|
| 27 |
function formatAlignLeft() { execFormat('justifyLeft'); }
|
|
|
|
| 3 |
|
| 4 |
/**
|
| 5 |
* Execute a formatting command on the current selection
|
| 6 |
+
* @param {string} command - execCommand name
|
| 7 |
+
* @param {string} [value] - optional value
|
| 8 |
+
* @param {boolean} [keepSelection] - if true, don't collapse selection
|
| 9 |
*/
|
| 10 |
+
function execFormat(command, value, keepSelection) {
|
| 11 |
+
document.execCommand(command, false, value !== undefined ? value : null);
|
| 12 |
const editor = getEditorElement();
|
| 13 |
if (editor) editor.focus();
|
| 14 |
+
|
| 15 |
+
// Collapse selection after formatting so text doesn't stay highlighted
|
| 16 |
+
if (!keepSelection) {
|
| 17 |
+
const sel = window.getSelection();
|
| 18 |
+
if (sel && sel.rangeCount > 0 && !sel.isCollapsed) {
|
| 19 |
+
sel.collapseToEnd();
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
updateFormatState();
|
| 24 |
}
|
| 25 |
|
|
|
|
| 30 |
function formatStrikethrough() { execFormat('strikethrough'); }
|
| 31 |
|
| 32 |
/* ── Undo / Redo (handles both typing and formatting) ── */
|
| 33 |
+
function formatUndo() { execFormat('undo', undefined, true); }
|
| 34 |
+
function formatRedo() { execFormat('redo', undefined, true); }
|
| 35 |
|
| 36 |
+
/* ── Alignment (applies to paragraph containing selection/cursor) ── */
|
| 37 |
function formatAlignRight() { execFormat('justifyRight'); }
|
| 38 |
function formatAlignCenter() { execFormat('justifyCenter'); }
|
| 39 |
function formatAlignLeft() { execFormat('justifyLeft'); }
|
src/js/selection.js
CHANGED
|
@@ -187,7 +187,9 @@ function setCaretOffset(offset) {
|
|
| 187 |
function getEditorText() {
|
| 188 |
const editor = document.getElementById('editor-container');
|
| 189 |
if (!editor) return '';
|
| 190 |
-
|
|
|
|
|
|
|
| 191 |
}
|
| 192 |
|
| 193 |
/**
|
|
|
|
| 187 |
function getEditorText() {
|
| 188 |
const editor = document.getElementById('editor-container');
|
| 189 |
if (!editor) return '';
|
| 190 |
+
// MUST use textContent to match the offset calculation in overlaySuggestions
|
| 191 |
+
// innerText adds '\n' for block elements which causes offset mismatch
|
| 192 |
+
return editor.textContent || '';
|
| 193 |
}
|
| 194 |
|
| 195 |
/**
|