refactor: extract selection anchor helpers

This commit is contained in:
lix-2026
2026-05-25 08:54:27 +08:00
parent f0c1757c9f
commit 193b3e1154
6 changed files with 145 additions and 84 deletions
@@ -1284,7 +1284,7 @@ function __wbg_get_imports() {
return ret;
},
__wbindgen_cast_0000000000000006: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 883, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 294, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf);
return ret;
},
@@ -4,7 +4,10 @@ use serde_json::Value;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{window, Element, Node};
use crate::editor_root_element;
use crate::{
editor_root_element,
editor_runtime::block_dnd::{direct_block_from_element, top_level_block_index},
};
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
@@ -54,6 +57,18 @@ pub(crate) fn has_active_text_selection() -> bool {
active_editor_text_selection().is_some()
}
pub(crate) fn block_index_from_selection() -> Option<usize> {
let selection = window().and_then(|win| win.get_selection().ok().flatten())?;
let anchor_node = selection.anchor_node()?;
let element = anchor_node
.dyn_ref::<Element>()
.cloned()
.or_else(|| anchor_node.parent_element())?;
let root = editor_root_element()?;
let block = direct_block_from_element(element, &root)?;
top_level_block_index(&root, &block)
}
pub(crate) fn selection_summary(selection: &TiptapSelectionState) -> String {
let block = if selection.h1 {
"一级标题"
@@ -6,7 +6,7 @@ use crate::{
editor_runtime::editor_focus::active_editor_stage,
editor_stage_element,
};
use web_sys::{window, Element};
use web_sys::{window, DomRect, Element};
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct FloatingToolbarAnchor {
@@ -136,9 +136,7 @@ pub(crate) fn table_selection_overlay_style(
}
pub(crate) fn floating_toolbar_anchor_from_selection() -> Option<FloatingToolbarAnchor> {
let selection = active_editor_text_selection()?;
let range = selection.get_range_at(0).ok()?;
let rect = range.get_bounding_client_rect();
let rect = selection_bounding_rect()?;
if rect.width() <= 0.0 && rect.height() <= 0.0 {
return None;
}
@@ -152,6 +150,83 @@ pub(crate) fn floating_toolbar_anchor_from_selection() -> Option<FloatingToolbar
})
}
pub(crate) fn image_toolbar_anchor_from_image(image: &Element) -> Option<ImageToolbarAnchor> {
let stage = editor_stage_element()?;
let rect = image.get_bounding_client_rect();
if rect.width() <= 0.0 && rect.height() <= 0.0 {
return None;
}
let stage_rect = stage.get_bounding_client_rect();
let align = image
.get_attribute("data-align")
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| "left".to_string());
Some(ImageToolbarAnchor {
top: (rect.top() - stage_rect.top()).max(60.0),
left: rect.left() + (rect.width() / 2.0) - stage_rect.left(),
align,
})
}
pub(crate) fn image_element_from_target(target: web_sys::EventTarget) -> Option<Element> {
let element = crate::editor_runtime::dom_events::target_element(target)?;
if element.tag_name().eq_ignore_ascii_case("img")
&& element
.closest(".editor-surface .ProseMirror")
.ok()
.flatten()
.is_some()
{
return Some(element);
}
element
.closest(".editor-surface .ProseMirror img[src]")
.ok()
.flatten()
}
pub(crate) fn selection_bounding_rect() -> Option<DomRect> {
let selection = active_editor_text_selection()?;
let range = selection.get_range_at(0).ok()?;
Some(range.get_bounding_client_rect())
}
pub(crate) fn viewport_dimensions() -> (f64, f64) {
let viewport_width = window()
.and_then(|win| win.inner_width().ok())
.and_then(|value| value.as_f64())
.unwrap_or(1024.0)
.max(320.0);
let viewport_height = window()
.and_then(|win| win.inner_height().ok())
.and_then(|value| value.as_f64())
.unwrap_or(768.0)
.max(240.0);
(viewport_width, viewport_height)
}
pub(crate) fn clamp_overlay_anchor(
raw_top: f64,
raw_left: f64,
raw_anchor_top: f64,
overlay_width: f64,
overlay_height: f64,
gap: f64,
) -> (f64, f64) {
let (viewport_width, viewport_height) = viewport_dimensions();
let clamped_left =
raw_left.clamp(gap, (viewport_width - overlay_width - gap).max(gap));
let below_limit = viewport_height - gap;
let clamped_top = if raw_top + overlay_height > below_limit {
(raw_anchor_top - overlay_height - gap).clamp(gap, below_limit - 120.0)
} else {
raw_top.clamp(gap, below_limit - 120.0)
};
(clamped_top, clamped_left)
}
pub(crate) fn sync_text_selection_overlay(
set_text_selection_active: WriteSignal<bool>,
set_floating_toolbar_anchor: WriteSignal<Option<FloatingToolbarAnchor>>,
+21 -78
View File
@@ -22,11 +22,10 @@ use web_sys::{
use editor_runtime::attachment_upload::dispatch_editor_upload_request;
use editor_runtime::block_dnd::{
block_state_from_index as block_state_from_index_runtime, direct_block_from_element,
block_state_from_index as block_state_from_index_runtime,
drop_indicator_from_point as drop_indicator_from_point_runtime,
drop_indicator_from_target as drop_indicator_from_target_runtime,
element_within_handle_shell, hovered_block_from_target as hovered_block_from_target_runtime,
top_level_block_index,
};
use editor_runtime::block_menu_overlay;
use editor_runtime::block_menu_document::{
@@ -43,16 +42,19 @@ use editor_runtime::command_sync::{
};
use editor_runtime::dom_events::{event_target_matches_selector, target_element};
use editor_runtime::dom_selection::{
runtime_block_id_from_index, selection_payload, selection_summary, SelectionPayload,
block_index_from_selection, runtime_block_id_from_index, selection_payload,
selection_summary, SelectionPayload,
};
use editor_runtime::editor_focus::{active_editor_stage, body_has_focus, schedule_editor_focus};
use editor_runtime::overlays::{
close_editor_floating_overlays_if_escape, open_block_menu_overlay, open_image_toolbar_overlay,
open_slash_menu_overlay, current_table_overlay_anchor, sync_overlays_on_selection_change,
table_col_aux_style, table_overlay_anchor_from_table, table_row_aux_style,
table_selection_overlay_style, toolbar_overlay_locked, try_sync_editor_overlay_state,
FloatingToolbarAnchor,
ImageToolbarAnchor, TableOverlayAnchor, TableSelectionKind, TableSelectionOverlayState,
clamp_overlay_anchor,
close_editor_floating_overlays_if_escape, current_table_overlay_anchor,
image_element_from_target, image_toolbar_anchor_from_image, open_block_menu_overlay,
open_image_toolbar_overlay, open_slash_menu_overlay, selection_bounding_rect,
sync_overlays_on_selection_change, table_col_aux_style, table_overlay_anchor_from_table,
table_row_aux_style, table_selection_overlay_style, toolbar_overlay_locked,
try_sync_editor_overlay_state, FloatingToolbarAnchor, ImageToolbarAnchor,
TableOverlayAnchor, TableSelectionKind, TableSelectionOverlayState,
};
use editor_runtime::persistence::{
load_persisted_document, normalize_identity_value, persist_document_state,
@@ -4853,16 +4855,7 @@ fn hovered_block_from_target(target: web_sys::EventTarget) -> Option<HoveredBloc
}
fn hovered_block_from_selection() -> Option<HoveredBlockState> {
let selection = window().and_then(|win| win.get_selection().ok().flatten())?;
let anchor_node = selection.anchor_node()?;
let element = anchor_node
.dyn_ref::<Element>()
.cloned()
.or_else(|| anchor_node.parent_element())?;
let root = editor_root_element()?;
let block = direct_block_from_element(element, &root)?;
let index = top_level_block_index(&root, &block)?;
block_state_from_index(index)
block_index_from_selection().and_then(block_state_from_index)
}
fn block_state_from_index(index: usize) -> Option<HoveredBlockState> {
@@ -4947,21 +4940,7 @@ fn slash_menu_anchor_style() -> String {
const MENU_HEIGHT: f64 = 430.0;
const GAP: f64 = 8.0;
let viewport_width = window()
.and_then(|win| win.inner_width().ok())
.and_then(|value| value.as_f64())
.unwrap_or(1024.0)
.max(320.0);
let viewport_height = window()
.and_then(|win| win.inner_height().ok())
.and_then(|value| value.as_f64())
.unwrap_or(768.0)
.max(240.0);
let selection_rect = window()
.and_then(|win| win.get_selection().ok().flatten())
.and_then(|selection| selection.get_range_at(0).ok())
.map(|range| range.get_bounding_client_rect())
let selection_rect = selection_bounding_rect()
.filter(|rect| rect.top() > 0.0 || rect.left() > 0.0 || rect.height() > 0.0);
let (raw_top, raw_left, raw_anchor_top) = if let Some(rect) = selection_rect {
@@ -4981,54 +4960,18 @@ fn slash_menu_anchor_style() -> String {
(24.0, 24.0, 24.0)
};
let clamped_left = raw_left.clamp(GAP, (viewport_width - MENU_WIDTH - GAP).max(GAP));
let below_limit = viewport_height - GAP;
let clamped_top = if raw_top + MENU_HEIGHT > below_limit {
(raw_anchor_top - MENU_HEIGHT - GAP).clamp(GAP, below_limit - 120.0)
} else {
raw_top.clamp(GAP, below_limit - 120.0)
};
let (clamped_top, clamped_left) = clamp_overlay_anchor(
raw_top,
raw_left,
raw_anchor_top,
MENU_WIDTH,
MENU_HEIGHT,
GAP,
);
format!("top:{clamped_top:.1}px;left:{clamped_left:.1}px;")
}
fn image_toolbar_anchor_from_image(image: &Element) -> Option<ImageToolbarAnchor> {
let stage = editor_stage_element()?;
let rect = image.get_bounding_client_rect();
if rect.width() <= 0.0 && rect.height() <= 0.0 {
return None;
}
let stage_rect = stage.get_bounding_client_rect();
let align = image
.get_attribute("data-align")
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| "left".to_string());
Some(ImageToolbarAnchor {
top: (rect.top() - stage_rect.top()).max(60.0),
left: rect.left() + (rect.width() / 2.0) - stage_rect.left(),
align,
})
}
fn image_element_from_target(target: web_sys::EventTarget) -> Option<Element> {
let element = target_element(target)?;
if element.tag_name().eq_ignore_ascii_case("img")
&& element
.closest(".editor-surface .ProseMirror")
.ok()
.flatten()
.is_some()
{
return Some(element);
}
element
.closest(".editor-surface .ProseMirror img[src]")
.ok()
.flatten()
}
fn drop_indicator_from_target(
target: web_sys::EventTarget,
client_y: i32,