refactor: extract selection anchor helpers
This commit is contained in:
@@ -291,9 +291,37 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
|
||||
- 修改:`editor_runtime/block_dnd.rs` 新增 `hovered_block_from_target(target, root, handle_selector)`,承接 `target_element`、handle shell 过滤、direct block index 解析和 `block_state_from_index(index)` 委托;`lib.rs` 的同名函数退化为只负责获取 `editor_root_element()` 后调用 runtime helper。
|
||||
- 边界:未迁 `hovered_block_from_selection`、`drop_indicator_from_target`、`drop_indicator_from_point`、drag/drop listener、keyboard signal 分支或 block reorder 行为;本批只外置共享 target -> `HoveredBlockState` 解析层。
|
||||
- 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、wasm release build、`wasm-bindgen`、临时当前源码服务 `MNOTE_WEB_BIND=127.0.0.1:3020 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3020 cargo run --manifest-path rust/Cargo.toml -p mnote-web`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3020 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3020 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3020 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。
|
||||
- 2026-05-25:Batch AM 只派发 1 个只读 Reasonix worker 审计 `hovered_block_from_selection` / `slash_menu_anchor_style` 的下一刀边界,使用独立 worktree `/mnt/Data1T/mnote-worktrees/0525-am-worker-a-5-29-selection-anchor-readonly`,run id `reasonix-2026-05-25T00-26-34-892Z-8f7e4a81`,并显式使用 `--no-memory-recall`。
|
||||
- Codex 已读取 Hindsight recall、`process-handoff.md/json`、`result.json`,并核对 worktree `git status --short` 为空;Reasonix 未修改源码。
|
||||
- 审计结论经 Codex 复核后采纳:`hovered_block_from_selection` 不适合迁入 `editor_runtime/block_dnd.rs`,更合理的下一刀是把“selection 锚点 -> 顶层块 index”抽成 `editor_runtime/dom_selection.rs` 中的纯 helper,再继续由 `lib.rs` 组装 `HoveredBlockState`。
|
||||
- Codex 主控完成最小机械切片:`editor_runtime/dom_selection.rs` 新增 `block_index_from_selection()`,封装 selection anchor、element、root、direct block 与 top-level index 解析;`lib.rs::hovered_block_from_selection` 退化为 `block_index_from_selection().and_then(block_state_from_index)`。
|
||||
- 边界:未迁 `HoveredBlockState` 本身、`slash_menu_anchor_style`、viewport clamp、`drop_indicator_from_target`、拖拽 listener、keyboard signal 分支或 block reorder 行为;本批只外置 selection -> block index 解析层。
|
||||
- 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、wasm release build、`wasm-bindgen`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。
|
||||
- 2026-05-25:Codex 主控继续收口 `slash_menu_anchor_style` 的纯 overlay helper。
|
||||
- 修改:`editor_runtime/overlays.rs` 新增 `selection_bounding_rect()`、`viewport_dimensions()`、`clamp_overlay_anchor(...)`;`lib.rs::slash_menu_anchor_style` 改为复用 selection rect helper 与位置 clamp helper,仍保留 fallback 到 `hovered_block_from_selection()`、`editor_stage_element()` 与 `content_text_left(stage_rect.width())` 的壳层编排。
|
||||
- 边界:未迁 `slash_menu_anchor_style` 整体、`content_text_left`、hovered block fallback 计算、DOM listener、overlay signal 或 slash menu 行为;本批只外置 selection rect / viewport / clamp 纯计算层。
|
||||
- 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、wasm release build、`wasm-bindgen`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。
|
||||
- 2026-05-25:主控重新验证当前未提交 `5-29` 切片,确认可作为独立批次继续推进。
|
||||
- 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、wasm release build、`wasm-bindgen`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`、`codegraph sync .`、`codegraph status . --json`。
|
||||
- 结果:上述命令全部通过,CodeGraph `pendingChanges=0`;当前主工作区只保留本批 `dom_selection.rs` / `overlays.rs` / `lib.rs` 与生成物改动,不再混入新的实现写集。
|
||||
- 2026-05-25:Batch BA 计划只派发 1 个只读 Reasonix worker 审计 `slash_menu_anchor_style` 剩余 fallback 与 DnD 边界。
|
||||
- 目标:判断 `hovered_block_from_selection` fallback 几何、`content_text_left(stage_rect.width())`、`drop_indicator_from_point` 当前是否还有可继续外置的纯 helper,避免在 `lib.rs` 残留第二套 overlay / DnD 几何路径。
|
||||
- 约束:只读、独立 worktree、源码零 diff;不得建议迁 listener、signal 编排、`HANDLE_SHELL_SELECTOR` 常量或 block reorder 行为。
|
||||
- 2026-05-25:Batch BA 只派发 1 个只读 Reasonix worker 审计 `slash_menu_anchor_style` 剩余 fallback 与 DnD 边界,使用独立 worktree `/mnt/Data1T/mnote-worktrees/0525-ba-worker-b-5-29-slash-drop-readonly`,run id `reasonix-2026-05-25T00-47-26-272Z-dbfa4fc7`,显式 `--no-memory-recall`。
|
||||
- Codex 已读取 Hindsight recall、`process-handoff.md/json`、`result.json`,并核对 worktree `git status --short` / `git diff --stat` 为空;worker 未修改源码。
|
||||
- 审计结论经主控复核后采纳:`drop_indicator_from_point` / `drop_indicator_from_target` 当前已经充分收口,`lib.rs` 中仅剩 `HANDLE_SHELL_SELECTOR` 注入薄壳;`slash_menu_anchor_style` 本体不宜整体迁出,但 `image_toolbar_anchor_from_image` / `image_element_from_target` 是下一刀最合适的纯 DOM/几何 helper。
|
||||
- 当前决定:按该结论继续推进最小机械切片,只迁 `image toolbar` 相关 helper,不扩散到 overlay signal 编排、listener、`content_text_left` 共享布局模块或 `drop_indicator_*` 壳层。
|
||||
- 2026-05-25:Codex 主控按 Batch BA 审计结论完成 `image toolbar` 纯 helper 小切片。
|
||||
- 修改:`editor_runtime/overlays.rs` 新增 `image_toolbar_anchor_from_image(image)`、`image_element_from_target(target)`;`lib.rs` 删除同名实现,仅保留调用点和 image toolbar 打开编排。
|
||||
- 边界:未迁 `open_image_toolbar_overlay`、image toolbar signal 编排、DOM listener、`slash_menu_anchor_style` 本体、`content_text_left` 共享布局常量或 `drop_indicator_*` 壳层;本批只外置 image target 解析与 image toolbar anchor 几何。
|
||||
- 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、wasm release build、`wasm-bindgen`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。
|
||||
- 2026-05-25:Batch AK 派发 1 个只读 Reasonix worker 审计 `drop_indicator_from_target` / `drop_indicator_from_point` 的最小外置边界,使用独立 worktree `/mnt/Data1T/mnote-worktrees/0525-ak-worker-a-5-29-drop-indicator-readonly`,run id `reasonix-2026-05-25T00-23-12-071Z-5cb35e3f`,显式 `--no-memory-recall`。
|
||||
- Codex 已读取 Hindsight recall、`process-handoff.md/json`,并核对 worktree `git status --short` / `git diff --stat` 为空;只读结论经主控复核后采纳。
|
||||
- 审计结论:`drop_indicator_from_target` 已具备迁入 `editor_runtime/block_dnd.rs` 的条件,`drop_indicator_from_point` 可同刀迁出;`hovered_block_from_target` 的 hover listener 壳与现有 drag/drop 调用点暂留 `lib.rs`,通过 `HANDLE_SHELL_SELECTOR` 参数注入 runtime helper。
|
||||
- Codex 主控完成最小机械切片:`editor_runtime/block_dnd.rs` 新增 `drop_indicator_from_target(target, client_y, handle_selector)`、`drop_indicator_from_point(client_x, client_y, handle_selector)`;`lib.rs` 同名函数退化为只透传 `HANDLE_SHELL_SELECTOR` 的薄壳,不扩散 drag/drop listener 或 block reorder 调用点。
|
||||
- 边界:未迁 `hovered_block_from_selection`、drag/drop listener、keyboard signal 分支、block reorder 行为或 `HANDLE_SHELL_SELECTOR` 常量定义;本批只外置 drop indicator 的 target/point 解析 helper。
|
||||
- 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、wasm release build、`wasm-bindgen --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm`、临时当前源码服务 `MNOTE_WEB_BIND=127.0.0.1:3032 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3032 cargo run --manifest-path rust/Cargo.toml -p mnote-web`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3032 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3032 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3032 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。
|
||||
- 2026-05-25:主控对当前未提交 `5-29` 切片重新做 fresh 验证,确认这批仍可独立提交。
|
||||
- 切片范围:`editor_runtime/dom_selection.rs` 新增 `block_index_from_selection()`;`editor_runtime/overlays.rs` 新增 `selection_bounding_rect()`、`viewport_dimensions()`、`clamp_overlay_anchor(...)`;`lib.rs` 的 `hovered_block_from_selection` / `slash_menu_anchor_style` 退化为调用上述 helper 的壳层编排;生成物只更新 `generated/island` 对应 `js/wasm`。
|
||||
- 本轮 fresh 验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、`cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release`、`wasm-bindgen --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm`、临时当前源码服务 `MNOTE_WEB_BIND=127.0.0.1:3021 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3021 cargo run --manifest-path rust/Cargo.toml -p mnote-web`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3021 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3021 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3021 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。
|
||||
- 结果:上述命令全部通过;`task479` 确认附件生命周期、主编辑区附件缺失态、文件树打开附件和“文件名作为本地 Markdown 默认页头标题”的基线行为未被本批回归。
|
||||
|
||||
+1
-1
@@ -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;
|
||||
},
|
||||
|
||||
BIN
Binary file not shown.
@@ -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