fix: message bug and context menu bugs
This commit is contained in:
parent
15cc1f708f
commit
84cb4e5e78
3 changed files with 76 additions and 19 deletions
|
|
@ -162,25 +162,24 @@ pub fn ChatInput(
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pre-fill with /whisper command
|
// Pre-fill with /whisper command prefix only (no placeholder text)
|
||||||
let placeholder = "your message here";
|
// User types their message after the space
|
||||||
let whisper_text = format!("/whisper {} {}", target_name, placeholder);
|
// parse_whisper_command already rejects empty messages
|
||||||
|
let whisper_prefix = format!("/whisper {} ", target_name);
|
||||||
|
|
||||||
if let Some(input) = input_ref.get() {
|
if let Some(input) = input_ref.get() {
|
||||||
// Set the message
|
// Set the message
|
||||||
set_message.set(whisper_text.clone());
|
set_message.set(whisper_prefix.clone());
|
||||||
|
// Don't show hint - user already knows they're whispering
|
||||||
set_command_mode.set(CommandMode::None);
|
set_command_mode.set(CommandMode::None);
|
||||||
|
|
||||||
// Update input value
|
// Update input value
|
||||||
input.set_value(&whisper_text);
|
input.set_value(&whisper_prefix);
|
||||||
|
|
||||||
// Focus the input
|
// Focus the input and position cursor at end
|
||||||
let _ = input.focus();
|
let _ = input.focus();
|
||||||
|
let len = whisper_prefix.len() as u32;
|
||||||
// Select the placeholder text so it gets replaced when typing
|
let _ = input.set_selection_range(len, len);
|
||||||
let prefix_len = format!("/whisper {} ", target_name).len() as u32;
|
|
||||||
let total_len = whisper_text.len() as u32;
|
|
||||||
let _ = input.set_selection_range(prefix_len, total_len);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -230,8 +229,24 @@ pub fn ChatInput(
|
||||||
let cmd = value[1..].to_lowercase();
|
let cmd = value[1..].to_lowercase();
|
||||||
|
|
||||||
// Show hint for slash commands (don't execute until Enter)
|
// Show hint for slash commands (don't execute until Enter)
|
||||||
// Match: /s[etting], /i[nventory], /w[hisper], or their full forms with args
|
// Match: /s[etting], /i[nventory], /w[hisper]
|
||||||
if cmd.is_empty()
|
// But NOT when whisper command is complete (has name + space for message)
|
||||||
|
let is_complete_whisper = {
|
||||||
|
// Check if it's "/w name " or "/whisper name " (name followed by space)
|
||||||
|
let rest = cmd.strip_prefix("whisper ").or_else(|| cmd.strip_prefix("w "));
|
||||||
|
if let Some(after_cmd) = rest {
|
||||||
|
// If there's content after the command and it contains a space,
|
||||||
|
// user has typed "name " and is now typing the message
|
||||||
|
after_cmd.contains(' ')
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_complete_whisper {
|
||||||
|
// User is typing the message part, no hint needed
|
||||||
|
set_command_mode.set(CommandMode::None);
|
||||||
|
} else if cmd.is_empty()
|
||||||
|| "setting".starts_with(&cmd)
|
|| "setting".starts_with(&cmd)
|
||||||
|| "inventory".starts_with(&cmd)
|
|| "inventory".starts_with(&cmd)
|
||||||
|| "whisper".starts_with(&cmd)
|
|| "whisper".starts_with(&cmd)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ pub struct ContextMenuItem {
|
||||||
/// Props:
|
/// Props:
|
||||||
/// - `open`: Whether the menu is currently visible
|
/// - `open`: Whether the menu is currently visible
|
||||||
/// - `position`: The (x, y) position in client coordinates where the menu should appear
|
/// - `position`: The (x, y) position in client coordinates where the menu should appear
|
||||||
|
/// - `header`: Optional header text displayed at the top (e.g., username)
|
||||||
/// - `items`: The menu items to display
|
/// - `items`: The menu items to display
|
||||||
/// - `on_select`: Callback when a menu item is selected, receives the action string
|
/// - `on_select`: Callback when a menu item is selected, receives the action string
|
||||||
/// - `on_close`: Callback when the menu should close (click outside, escape, etc.)
|
/// - `on_close`: Callback when the menu should close (click outside, escape, etc.)
|
||||||
|
|
@ -35,6 +36,9 @@ pub fn ContextMenu(
|
||||||
/// Position (x, y) in client coordinates.
|
/// Position (x, y) in client coordinates.
|
||||||
#[prop(into)]
|
#[prop(into)]
|
||||||
position: Signal<(f64, f64)>,
|
position: Signal<(f64, f64)>,
|
||||||
|
/// Optional header text (e.g., username) displayed above the menu items.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
header: Option<Signal<Option<String>>>,
|
||||||
/// Menu items to display.
|
/// Menu items to display.
|
||||||
#[prop(into)]
|
#[prop(into)]
|
||||||
items: Signal<Vec<ContextMenuItem>>,
|
items: Signal<Vec<ContextMenuItem>>,
|
||||||
|
|
@ -82,12 +86,40 @@ pub fn ContextMenu(
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Click outside handler
|
// Click outside handler - use Effect with cleanup to properly remove handlers
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
{
|
{
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
use wasm_bindgen::{JsCast, closure::Closure};
|
use wasm_bindgen::{JsCast, closure::Closure};
|
||||||
|
|
||||||
|
// Store closures so we can remove them on cleanup
|
||||||
|
let mousedown_closure: Rc<RefCell<Option<Closure<dyn Fn(web_sys::MouseEvent)>>>> =
|
||||||
|
Rc::new(RefCell::new(None));
|
||||||
|
let keydown_closure: Rc<RefCell<Option<Closure<dyn Fn(web_sys::KeyboardEvent)>>>> =
|
||||||
|
Rc::new(RefCell::new(None));
|
||||||
|
|
||||||
|
let mousedown_closure_clone = mousedown_closure.clone();
|
||||||
|
let keydown_closure_clone = keydown_closure.clone();
|
||||||
|
|
||||||
Effect::new(move |_| {
|
Effect::new(move |_| {
|
||||||
|
let window = web_sys::window().unwrap();
|
||||||
|
|
||||||
|
// Clean up previous handlers first
|
||||||
|
if let Some(old_handler) = mousedown_closure_clone.borrow_mut().take() {
|
||||||
|
let _ = window.remove_event_listener_with_callback(
|
||||||
|
"mousedown",
|
||||||
|
old_handler.as_ref().unchecked_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(old_handler) = keydown_closure_clone.borrow_mut().take() {
|
||||||
|
let _ = window.remove_event_listener_with_callback(
|
||||||
|
"keydown",
|
||||||
|
old_handler.as_ref().unchecked_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add handlers when menu is open
|
||||||
if !open.get() {
|
if !open.get() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -100,6 +132,7 @@ pub fn ContextMenu(
|
||||||
let menu_el: web_sys::HtmlElement = menu_el.into();
|
let menu_el: web_sys::HtmlElement = menu_el.into();
|
||||||
let menu_el_clone = menu_el.clone();
|
let menu_el_clone = menu_el.clone();
|
||||||
|
|
||||||
|
// Mousedown handler for click-outside detection
|
||||||
let handler =
|
let handler =
|
||||||
Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |ev: web_sys::MouseEvent| {
|
Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |ev: web_sys::MouseEvent| {
|
||||||
if let Some(target) = ev.target() {
|
if let Some(target) = ev.target() {
|
||||||
|
|
@ -111,9 +144,9 @@ pub fn ContextMenu(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let window = web_sys::window().unwrap();
|
|
||||||
let _ = window
|
let _ = window
|
||||||
.add_event_listener_with_callback("mousedown", handler.as_ref().unchecked_ref());
|
.add_event_listener_with_callback("mousedown", handler.as_ref().unchecked_ref());
|
||||||
|
*mousedown_closure_clone.borrow_mut() = Some(handler);
|
||||||
|
|
||||||
// Escape key handler
|
// Escape key handler
|
||||||
let on_close_esc = on_close.clone();
|
let on_close_esc = on_close.clone();
|
||||||
|
|
@ -129,10 +162,7 @@ pub fn ContextMenu(
|
||||||
"keydown",
|
"keydown",
|
||||||
keydown_handler.as_ref().unchecked_ref(),
|
keydown_handler.as_ref().unchecked_ref(),
|
||||||
);
|
);
|
||||||
|
*keydown_closure_clone.borrow_mut() = Some(keydown_handler);
|
||||||
// Store handlers to clean up (they get cleaned up when Effect reruns)
|
|
||||||
handler.forget();
|
|
||||||
keydown_handler.forget();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,6 +175,17 @@ pub fn ContextMenu(
|
||||||
role="menu"
|
role="menu"
|
||||||
aria-label="Context menu"
|
aria-label="Context menu"
|
||||||
>
|
>
|
||||||
|
// Header with username and divider
|
||||||
|
{move || {
|
||||||
|
header.and_then(|h| h.get()).map(|header_text| {
|
||||||
|
view! {
|
||||||
|
<div class="px-4 py-2 text-center">
|
||||||
|
<div class="text-sm font-semibold text-white">{header_text}</div>
|
||||||
|
</div>
|
||||||
|
<hr class="border-gray-600 mx-2" />
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
<For
|
<For
|
||||||
each=move || items.get()
|
each=move || items.get()
|
||||||
key=|item| item.action.clone()
|
key=|item| item.action.clone()
|
||||||
|
|
|
||||||
|
|
@ -1033,6 +1033,7 @@ pub fn RealmSceneViewer(
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
open=Signal::derive(move || context_menu_open.get())
|
open=Signal::derive(move || context_menu_open.get())
|
||||||
position=Signal::derive(move || context_menu_position.get())
|
position=Signal::derive(move || context_menu_position.get())
|
||||||
|
header=Signal::derive(move || context_menu_target.get())
|
||||||
items=Signal::derive(move || {
|
items=Signal::derive(move || {
|
||||||
vec![
|
vec![
|
||||||
ContextMenuItem {
|
ContextMenuItem {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue