fix: 浮动工具栏水平/垂直定位完全重写 — 基于等宽字体精确计算列偏移+滚动补偿
CI / test (18.x) (push) Canceled after 0s
CI / test (20.x) (push) Canceled after 0s
CI / test (22.x) (push) Canceled after 0s
CI / test (24.x) (push) Canceled after 0s

This commit is contained in:
2026-07-25 15:25:30 +08:00
parent a0bf5f3032
commit 60588745d3
8 changed files with 65 additions and 49 deletions
+15 -11
View File
@@ -3715,27 +3715,31 @@ class MarkdownEditor {
// Position the toolbar near the selection (toolbar is absolute inside editorPane) // Position the toolbar near the selection (toolbar is absolute inside editorPane)
const taRect = ta.getBoundingClientRect(); const taRect = ta.getBoundingClientRect();
const paneRect = this.editorPane.getBoundingClientRect(); const paneRect = this.editorPane.getBoundingClientRect();
const taStyle = getComputedStyle(ta);
const lineHeight = parseFloat(taStyle.lineHeight) || 22;
const paddingTop = parseFloat(taStyle.paddingTop) || 16;
const paddingLeft = parseFloat(taStyle.paddingLeft) || 18;
const fontSize = parseFloat(taStyle.fontSize) || 13.5;
// Monospace: char width ≈ fontSize * 0.6
const charWidth = fontSize * 0.6;
const textBefore = ta.value.substring(0, start); const textBefore = ta.value.substring(0, start);
const lines = textBefore.split('\n'); const lines = textBefore.split('\n');
const currentLine = lines.length - 1; const currentLine = lines.length - 1;
const lineHeight = parseInt(getComputedStyle(ta).lineHeight, 10) || 22; const currentCol = textBefore.length - textBefore.lastIndexOf('\n') - 1;
const paddingTop = parseInt(getComputedStyle(ta).paddingTop, 10) || 16; // Middle column of selection
const scrollOffset = ta.scrollTop; const midCol = currentCol + Math.floor((end - start) / 2 * ((ta.selectionDirection === 'backward') ? -1 : 1));
// Line top relative to viewport // Vertical: line position in textarea content, then convert to pane-local
const lineViewportTop = taRect.top + paddingTop + currentLine * lineHeight - scrollOffset; const lineViewportTop = taRect.top + paddingTop + currentLine * lineHeight - ta.scrollTop;
// Convert to editorPane-local coordinates
const lineLocalTop = lineViewportTop - paneRect.top; const lineLocalTop = lineViewportTop - paneRect.top;
const toolbarHeight = 36; const toolbarHeight = 36;
const gap = 4; const gap = 4;
// Try above first
let top = lineLocalTop - toolbarHeight - gap; let top = lineLocalTop - toolbarHeight - gap;
// If not enough room above, place below the selection
if (top < gap) { if (top < gap) {
top = lineLocalTop + lineHeight + gap; top = lineLocalTop + lineHeight + gap;
} }
// Horizontal: center near selection, in pane-local coords // Horizontal: column position in textarea content, convert to pane-local
const selMidX = taRect.left + (ta.selectionEnd - ta.selectionStart) / 2 * (taRect.width / Math.max(1, ta.value.length || 1)); const colX = taRect.left + paddingLeft + midCol * charWidth - ta.scrollLeft;
let left = selMidX - paneRect.left - 80; // ~half toolbar width (5*28 + gaps) let left = colX - paneRect.left - 80;
left = Math.max(4, Math.min(left, paneRect.width - 200)); left = Math.max(4, Math.min(left, paneRect.width - 200));
this._floatingToolbar.style.top = top + 'px'; this._floatingToolbar.style.top = top + 'px';
this._floatingToolbar.style.left = left + 'px'; this._floatingToolbar.style.left = left + 'px';
+1 -1
View File
File diff suppressed because one or more lines are too long
+15 -11
View File
@@ -3711,27 +3711,31 @@ class MarkdownEditor {
// Position the toolbar near the selection (toolbar is absolute inside editorPane) // Position the toolbar near the selection (toolbar is absolute inside editorPane)
const taRect = ta.getBoundingClientRect(); const taRect = ta.getBoundingClientRect();
const paneRect = this.editorPane.getBoundingClientRect(); const paneRect = this.editorPane.getBoundingClientRect();
const taStyle = getComputedStyle(ta);
const lineHeight = parseFloat(taStyle.lineHeight) || 22;
const paddingTop = parseFloat(taStyle.paddingTop) || 16;
const paddingLeft = parseFloat(taStyle.paddingLeft) || 18;
const fontSize = parseFloat(taStyle.fontSize) || 13.5;
// Monospace: char width ≈ fontSize * 0.6
const charWidth = fontSize * 0.6;
const textBefore = ta.value.substring(0, start); const textBefore = ta.value.substring(0, start);
const lines = textBefore.split('\n'); const lines = textBefore.split('\n');
const currentLine = lines.length - 1; const currentLine = lines.length - 1;
const lineHeight = parseInt(getComputedStyle(ta).lineHeight, 10) || 22; const currentCol = textBefore.length - textBefore.lastIndexOf('\n') - 1;
const paddingTop = parseInt(getComputedStyle(ta).paddingTop, 10) || 16; // Middle column of selection
const scrollOffset = ta.scrollTop; const midCol = currentCol + Math.floor((end - start) / 2 * ((ta.selectionDirection === 'backward') ? -1 : 1));
// Line top relative to viewport // Vertical: line position in textarea content, then convert to pane-local
const lineViewportTop = taRect.top + paddingTop + currentLine * lineHeight - scrollOffset; const lineViewportTop = taRect.top + paddingTop + currentLine * lineHeight - ta.scrollTop;
// Convert to editorPane-local coordinates
const lineLocalTop = lineViewportTop - paneRect.top; const lineLocalTop = lineViewportTop - paneRect.top;
const toolbarHeight = 36; const toolbarHeight = 36;
const gap = 4; const gap = 4;
// Try above first
let top = lineLocalTop - toolbarHeight - gap; let top = lineLocalTop - toolbarHeight - gap;
// If not enough room above, place below the selection
if (top < gap) { if (top < gap) {
top = lineLocalTop + lineHeight + gap; top = lineLocalTop + lineHeight + gap;
} }
// Horizontal: center near selection, in pane-local coords // Horizontal: column position in textarea content, convert to pane-local
const selMidX = taRect.left + (ta.selectionEnd - ta.selectionStart) / 2 * (taRect.width / Math.max(1, ta.value.length || 1)); const colX = taRect.left + paddingLeft + midCol * charWidth - ta.scrollLeft;
let left = selMidX - paneRect.left - 80; // ~half toolbar width (5*28 + gaps) let left = colX - paneRect.left - 80;
left = Math.max(4, Math.min(left, paneRect.width - 200)); left = Math.max(4, Math.min(left, paneRect.width - 200));
this._floatingToolbar.style.top = top + 'px'; this._floatingToolbar.style.top = top + 'px';
this._floatingToolbar.style.left = left + 'px'; this._floatingToolbar.style.left = left + 'px';
+1 -1
View File
File diff suppressed because one or more lines are too long
+15 -11
View File
@@ -3717,27 +3717,31 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
// Position the toolbar near the selection (toolbar is absolute inside editorPane) // Position the toolbar near the selection (toolbar is absolute inside editorPane)
const taRect = ta.getBoundingClientRect(); const taRect = ta.getBoundingClientRect();
const paneRect = this.editorPane.getBoundingClientRect(); const paneRect = this.editorPane.getBoundingClientRect();
const taStyle = getComputedStyle(ta);
const lineHeight = parseFloat(taStyle.lineHeight) || 22;
const paddingTop = parseFloat(taStyle.paddingTop) || 16;
const paddingLeft = parseFloat(taStyle.paddingLeft) || 18;
const fontSize = parseFloat(taStyle.fontSize) || 13.5;
// Monospace: char width ≈ fontSize * 0.6
const charWidth = fontSize * 0.6;
const textBefore = ta.value.substring(0, start); const textBefore = ta.value.substring(0, start);
const lines = textBefore.split('\n'); const lines = textBefore.split('\n');
const currentLine = lines.length - 1; const currentLine = lines.length - 1;
const lineHeight = parseInt(getComputedStyle(ta).lineHeight, 10) || 22; const currentCol = textBefore.length - textBefore.lastIndexOf('\n') - 1;
const paddingTop = parseInt(getComputedStyle(ta).paddingTop, 10) || 16; // Middle column of selection
const scrollOffset = ta.scrollTop; const midCol = currentCol + Math.floor((end - start) / 2 * ((ta.selectionDirection === 'backward') ? -1 : 1));
// Line top relative to viewport // Vertical: line position in textarea content, then convert to pane-local
const lineViewportTop = taRect.top + paddingTop + currentLine * lineHeight - scrollOffset; const lineViewportTop = taRect.top + paddingTop + currentLine * lineHeight - ta.scrollTop;
// Convert to editorPane-local coordinates
const lineLocalTop = lineViewportTop - paneRect.top; const lineLocalTop = lineViewportTop - paneRect.top;
const toolbarHeight = 36; const toolbarHeight = 36;
const gap = 4; const gap = 4;
// Try above first
let top = lineLocalTop - toolbarHeight - gap; let top = lineLocalTop - toolbarHeight - gap;
// If not enough room above, place below the selection
if (top < gap) { if (top < gap) {
top = lineLocalTop + lineHeight + gap; top = lineLocalTop + lineHeight + gap;
} }
// Horizontal: center near selection, in pane-local coords // Horizontal: column position in textarea content, convert to pane-local
const selMidX = taRect.left + (ta.selectionEnd - ta.selectionStart) / 2 * (taRect.width / Math.max(1, ta.value.length || 1)); const colX = taRect.left + paddingLeft + midCol * charWidth - ta.scrollLeft;
let left = selMidX - paneRect.left - 80; // ~half toolbar width (5*28 + gaps) let left = colX - paneRect.left - 80;
left = Math.max(4, Math.min(left, paneRect.width - 200)); left = Math.max(4, Math.min(left, paneRect.width - 200));
this._floatingToolbar.style.top = top + 'px'; this._floatingToolbar.style.top = top + 'px';
this._floatingToolbar.style.left = left + 'px'; this._floatingToolbar.style.left = left + 'px';
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+16 -12
View File
@@ -731,30 +731,34 @@ export class MarkdownEditor {
// Position the toolbar near the selection (toolbar is absolute inside editorPane) // Position the toolbar near the selection (toolbar is absolute inside editorPane)
const taRect = ta.getBoundingClientRect(); const taRect = ta.getBoundingClientRect();
const paneRect = this.editorPane.getBoundingClientRect(); const paneRect = this.editorPane.getBoundingClientRect();
const taStyle = getComputedStyle(ta);
const lineHeight = parseFloat(taStyle.lineHeight) || 22;
const paddingTop = parseFloat(taStyle.paddingTop) || 16;
const paddingLeft = parseFloat(taStyle.paddingLeft) || 18;
const fontSize = parseFloat(taStyle.fontSize) || 13.5;
// Monospace: char width ≈ fontSize * 0.6
const charWidth = fontSize * 0.6;
const textBefore = ta.value.substring(0, start); const textBefore = ta.value.substring(0, start);
const lines = textBefore.split('\n'); const lines = textBefore.split('\n');
const currentLine = lines.length - 1; const currentLine = lines.length - 1;
const lineHeight = parseInt(getComputedStyle(ta).lineHeight, 10) || 22; const currentCol = textBefore.length - textBefore.lastIndexOf('\n') - 1;
const paddingTop = parseInt(getComputedStyle(ta).paddingTop, 10) || 16; // Middle column of selection
const scrollOffset = ta.scrollTop; const midCol = currentCol + Math.floor((end - start) / 2 * ((ta.selectionDirection === 'backward') ? -1 : 1));
// Line top relative to viewport // Vertical: line position in textarea content, then convert to pane-local
const lineViewportTop = taRect.top + paddingTop + currentLine * lineHeight - scrollOffset; const lineViewportTop = taRect.top + paddingTop + currentLine * lineHeight - ta.scrollTop;
// Convert to editorPane-local coordinates
const lineLocalTop = lineViewportTop - paneRect.top; const lineLocalTop = lineViewportTop - paneRect.top;
const toolbarHeight = 36; const toolbarHeight = 36;
const gap = 4; const gap = 4;
// Try above first
let top = lineLocalTop - toolbarHeight - gap; let top = lineLocalTop - toolbarHeight - gap;
// If not enough room above, place below the selection
if (top < gap) { if (top < gap) {
top = lineLocalTop + lineHeight + gap; top = lineLocalTop + lineHeight + gap;
} }
// Horizontal: center near selection, in pane-local coords // Horizontal: column position in textarea content, convert to pane-local
const selMidX = taRect.left + (ta.selectionEnd - ta.selectionStart) / 2 * (taRect.width / Math.max(1, ta.value.length || 1)); const colX = taRect.left + paddingLeft + midCol * charWidth - ta.scrollLeft;
let left = selMidX - paneRect.left - 80; // ~half toolbar width (5*28 + gaps) let left = colX - paneRect.left - 80;
left = Math.max(4, Math.min(left, paneRect.width - 200)); left = Math.max(4, Math.min(left, paneRect.width - 200));
this._floatingToolbar!.style.top = top + 'px'; this._floatingToolbar!.style.top = top + 'px';