Passing detail:clickCount to MouseEvent is necessary but not sufficient for triple-click to select all text. The select-all-on-triple-click is a browser native behavior that synthetic MouseEvent dispatch doesn't trigger. Add manual selection after click dispatch: when clickCount >= 3 and the target is an INPUT or TEXTAREA, set selectionStart=0 and selectionEnd=value.length.
195 lines
12 KiB
Rust
195 lines
12 KiB
Rust
use serde_json::{json, Value};
|
|
|
|
use crate::dispatch::CdpContext;
|
|
|
|
pub async fn handle(
|
|
method: &str,
|
|
params: &Value,
|
|
ctx: &mut CdpContext,
|
|
session_id: &Option<String>,
|
|
) -> Result<Value, String> {
|
|
match method {
|
|
"dispatchMouseEvent" => {
|
|
let event_type = params.get("type").and_then(|v| v.as_str()).unwrap_or("");
|
|
let x = params.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
|
let y = params.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
|
let _button = params.get("button").and_then(|v| v.as_str()).unwrap_or("left");
|
|
let click_count = params.get("clickCount").and_then(|v| v.as_u64()).unwrap_or(1);
|
|
|
|
if event_type == "mousePressed" {
|
|
if let Some(page) = ctx.get_session_page_mut(session_id) {
|
|
let code = format!(
|
|
"(function() {{\
|
|
var target = (document.elementFromPoint && document.elementFromPoint({x},{y})) || globalThis.__obscura_click_target || document.activeElement || document.body;\
|
|
if (!target) return;\
|
|
globalThis.__obscura_click_target = target;\
|
|
var evt = globalThis.__obscura_markTrusted(new MouseEvent('mousedown', {{bubbles:true,cancelable:true,clientX:{x},clientY:{y},button:0,detail:{click_count}}}));\
|
|
target.dispatchEvent(evt);\
|
|
var click = globalThis.__obscura_markTrusted(new MouseEvent('click', {{bubbles:true,cancelable:true,clientX:{x},clientY:{y},button:0,detail:{click_count}}}));\
|
|
var cancelled = !target.dispatchEvent(click);\
|
|
if (!cancelled) {{\
|
|
var link = target.closest ? target.closest('a[href]') : null;\
|
|
if (!link && target.tagName === 'A' && target.getAttribute('href')) link = target;\
|
|
if (link) {{\
|
|
var href = link.getAttribute('href');\
|
|
if (href && !href.startsWith('#') && !href.startsWith('javascript:')) {{\
|
|
location.assign(href);\
|
|
}}\
|
|
}} else {{\
|
|
var tag = target.tagName;\
|
|
var type = (target.getAttribute && target.getAttribute('type') || '').toLowerCase();\
|
|
if (tag === 'BUTTON' && type !== 'button' && type !== 'reset') {{\
|
|
var form = target.closest ? target.closest('form') : null;\
|
|
if (form && typeof form.submit === 'function') {{ try {{ form.submit(target); }} catch(e) {{}} }}\
|
|
}} else if (tag === 'INPUT' && (type === 'submit' || type === 'image')) {{\
|
|
var form2 = target.closest ? target.closest('form') : null;\
|
|
if (form2 && typeof form2.submit === 'function') {{ try {{ form2.submit(target); }} catch(e) {{}} }}\
|
|
}} else if (tag === 'INPUT' && (type === 'checkbox' || type === 'radio')) {{\
|
|
target.checked = !target.checked;\
|
|
try {{ target.dispatchEvent(globalThis.__obscura_markTrusted(new Event('change', {{bubbles:true}}))); }} catch(e) {{}}\
|
|
}} else if ({click_count} >= 3 && (tag === 'INPUT' || tag === 'TEXTAREA')) {{\
|
|
// Triple-click selects all text (browser native behavior not replicated
|
|
// by synthetic MouseEvent, so we do it manually).
|
|
var len = target.value ? target.value.length : 0;\
|
|
if (target.setSelectionRange) {{\
|
|
target.setSelectionRange(0, len);\
|
|
}} else {{\
|
|
target.selectionStart = 0;\
|
|
target.selectionEnd = len;\
|
|
}}\
|
|
}}\
|
|
}}\
|
|
}}\
|
|
}})()",
|
|
x = x, y = y, click_count = click_count,
|
|
);
|
|
page.evaluate(&code);
|
|
page.process_pending_navigation().await.map_err(|e| e.to_string())?;
|
|
}
|
|
} else if event_type == "mouseReleased" {
|
|
if let Some(page) = ctx.get_session_page_mut(session_id) {
|
|
let code = format!(
|
|
"(function() {{\
|
|
var target = (document.elementFromPoint && document.elementFromPoint({x},{y})) || globalThis.__obscura_click_target || document.activeElement || document.body;\
|
|
if (!target) return;\
|
|
var evt = globalThis.__obscura_markTrusted(new MouseEvent('mouseup', {{bubbles:true,cancelable:true,clientX:{x},clientY:{y},button:0}}));\
|
|
target.dispatchEvent(evt);\
|
|
}})()",
|
|
x = x, y = y,
|
|
);
|
|
page.evaluate(&code);
|
|
}
|
|
}
|
|
|
|
Ok(json!({}))
|
|
}
|
|
"dispatchKeyEvent" => {
|
|
let event_type = params.get("type").and_then(|v| v.as_str()).unwrap_or("");
|
|
let key = params.get("key").and_then(|v| v.as_str()).unwrap_or("");
|
|
let code = params.get("code").and_then(|v| v.as_str()).unwrap_or("");
|
|
let text = params.get("text").and_then(|v| v.as_str()).unwrap_or("");
|
|
|
|
if let Some(page) = ctx.get_session_page_mut(session_id) {
|
|
match event_type {
|
|
"keyDown" | "rawKeyDown" => {
|
|
let js = format!(
|
|
"(function() {{\
|
|
var target = document.activeElement || document.body;\
|
|
var evt = globalThis.__obscura_markTrusted(new KeyboardEvent('keydown', {{bubbles:true,cancelable:true,key:'{key}',code:'{code}'}}));\
|
|
target.dispatchEvent(evt);\
|
|
}})()",
|
|
key = key.replace('\'', "\\'"),
|
|
code = code.replace('\'', "\\'"),
|
|
);
|
|
page.evaluate(&js);
|
|
|
|
if !text.is_empty() && text != "\r" && text != "\n" {
|
|
// Need to escape backslash BEFORE single-quote so the new
|
|
// backslashes from quote escaping don't get double-escaped.
|
|
let escaped_text = text.replace('\\', "\\\\").replace('\'', "\\'");
|
|
let js = format!(
|
|
"(function() {{\
|
|
var target = document.activeElement;\
|
|
if (target && (target.localName === 'input' || target.localName === 'textarea')) {{\
|
|
target.value = (target.value || '') + '{text}';\
|
|
target.dispatchEvent(globalThis.__obscura_markTrusted(new Event('input', {{bubbles:true}})));\
|
|
}}\
|
|
}})()",
|
|
text = escaped_text,
|
|
);
|
|
page.evaluate(&js);
|
|
}
|
|
|
|
if key == "Enter" {
|
|
// In a textarea Enter inserts a newline; in input fields
|
|
// it submits the containing form. Real Chrome distinguishes
|
|
// these two and we should too: previously every Enter tried
|
|
// to submit the nearest form even from a textarea.
|
|
let js = "(function() {\
|
|
var target = document.activeElement;\
|
|
if (!target) return;\
|
|
target.dispatchEvent(globalThis.__obscura_markTrusted(new KeyboardEvent('keypress', {bubbles:true,key:'Enter',code:'Enter'})));\
|
|
if (target.localName === 'textarea') {\
|
|
target.value = (target.value || '') + '\\n';\
|
|
target.dispatchEvent(globalThis.__obscura_markTrusted(new Event('input', {bubbles:true})));\
|
|
} else {\
|
|
var form = target.form || (target.closest && target.closest('form'));\
|
|
if (form && typeof form.submit === 'function') form.submit();\
|
|
}\
|
|
})()";
|
|
page.evaluate(js);
|
|
}
|
|
|
|
if key == "Backspace" {
|
|
let js = "(function() {\
|
|
var target = document.activeElement;\
|
|
if (target && (target.localName === 'input' || target.localName === 'textarea')) {\
|
|
target.value = target.value.slice(0, -1);\
|
|
target.dispatchEvent(globalThis.__obscura_markTrusted(new Event('input', {bubbles:true})));\
|
|
}\
|
|
})()";
|
|
page.evaluate(js);
|
|
}
|
|
}
|
|
"keyUp" => {
|
|
let js = format!(
|
|
"(function() {{\
|
|
var target = document.activeElement || document.body;\
|
|
var evt = globalThis.__obscura_markTrusted(new KeyboardEvent('keyup', {{bubbles:true,key:'{key}',code:'{code}'}}));\
|
|
target.dispatchEvent(evt);\
|
|
}})()",
|
|
key = key.replace('\'', "\\'"),
|
|
code = code.replace('\'', "\\'"),
|
|
);
|
|
page.evaluate(&js);
|
|
}
|
|
"char" => {
|
|
if !text.is_empty() {
|
|
let escaped_text = text.replace('\\', "\\\\").replace('\'', "\\'");
|
|
let js = format!(
|
|
"(function() {{\
|
|
var target = document.activeElement;\
|
|
if (target && (target.localName === 'input' || target.localName === 'textarea')) {{\
|
|
target.value = (target.value || '') + '{text}';\
|
|
target.dispatchEvent(globalThis.__obscura_markTrusted(new Event('input', {{bubbles:true}})));\
|
|
}}\
|
|
}})()",
|
|
text = escaped_text,
|
|
);
|
|
page.evaluate(&js);
|
|
// Pump event loop so Angular change detection picks up the input
|
|
page.settle(50).await;
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
Ok(json!({}))
|
|
}
|
|
"dispatchTouchEvent" => Ok(json!({})),
|
|
"setIgnoreInputEvents" => Ok(json!({})),
|
|
_ => Err(format!("Unknown Input method: {}", method)),
|
|
}
|
|
}
|