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
@@ -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>>,