refactor: extract selection anchor helpers
This commit is contained in:
@@ -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>>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user