mirror of
https://github.com/m1ngsama/TUT.git
synced 2026-02-08 09:04:04 +00:00
feat: Add interactive dropdown selection for forms
- Parse and store OPTION elements in SELECT fields - Display selected option text in dropdown UI - Add SELECT_OPTION input mode for dropdown navigation - Support Enter on SELECT to enter selection mode - Use j/k or arrow keys to navigate through options - Enter to confirm selection, Esc to cancel - Auto-select first option or option marked with 'selected' - Real-time option preview in status bar - Status bar shows "-- SELECT --" mode Data structure: - Added options vector to DomNode (value, text pairs) - Added selected_option index to track current selection Keyboard shortcuts in SELECT mode: - j/Down: Next option - k/Up: Previous option - Enter: Select current option - Esc: Cancel selection
This commit is contained in:
parent
7e55ade793
commit
58b7607074
7 changed files with 120 additions and 6 deletions
|
|
@ -457,6 +457,7 @@ public:
|
|||
case InputMode::COMMAND:
|
||||
case InputMode::SEARCH: mode_str = input_handler.get_buffer(); break;
|
||||
case InputMode::FORM_EDIT: mode_str = "-- INSERT -- " + input_handler.get_buffer(); break;
|
||||
case InputMode::SELECT_OPTION: mode_str = "-- SELECT --"; break;
|
||||
default: mode_str = ""; break;
|
||||
}
|
||||
framebuffer->set_text(1, y, mode_str, colors::STATUSBAR_FG, colors::STATUSBAR_BG);
|
||||
|
|
@ -554,6 +555,10 @@ public:
|
|||
// Toggle checkbox
|
||||
field->checked = !field->checked;
|
||||
status_message = field->checked ? "☑ Checked" : "☐ Unchecked";
|
||||
} else if (field->input_type == "select") {
|
||||
// Enter dropdown selection mode
|
||||
input_handler.set_mode(InputMode::SELECT_OPTION);
|
||||
status_message = "-- SELECT -- (j/k to navigate, Enter to select, Esc to cancel)";
|
||||
} else if (field->input_type == "submit" || field->element_type == ElementType::BUTTON) {
|
||||
// TODO: Submit form
|
||||
status_message = "Form submit (not yet implemented)";
|
||||
|
|
@ -688,6 +693,37 @@ public:
|
|||
}
|
||||
break;
|
||||
|
||||
case Action::NEXT_OPTION:
|
||||
if (active_field >= 0 && active_field < static_cast<int>(current_tree.form_fields.size())) {
|
||||
auto* field = current_tree.form_fields[active_field];
|
||||
if (field && field->input_type == "select" && !field->options.empty()) {
|
||||
field->selected_option = (field->selected_option + 1) % field->options.size();
|
||||
status_message = "Option: " + field->options[field->selected_option].second;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Action::PREV_OPTION:
|
||||
if (active_field >= 0 && active_field < static_cast<int>(current_tree.form_fields.size())) {
|
||||
auto* field = current_tree.form_fields[active_field];
|
||||
if (field && field->input_type == "select" && !field->options.empty()) {
|
||||
field->selected_option = (field->selected_option - 1 + field->options.size()) %
|
||||
field->options.size();
|
||||
status_message = "Option: " + field->options[field->selected_option].second;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Action::SELECT_CURRENT_OPTION:
|
||||
if (active_field >= 0 && active_field < static_cast<int>(current_tree.form_fields.size())) {
|
||||
auto* field = current_tree.form_fields[active_field];
|
||||
if (field && field->input_type == "select" && !field->options.empty()) {
|
||||
field->value = field->options[field->selected_option].first;
|
||||
status_message = "Selected: " + field->options[field->selected_option].second;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Action::QUIT:
|
||||
break; // 在main loop处理
|
||||
|
||||
|
|
|
|||
|
|
@ -369,7 +369,30 @@ std::unique_ptr<DomNode> DomTreeBuilder::convert_node(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// For SELECT, collect all OPTION children
|
||||
if (element.tag == GUMBO_TAG_SELECT) {
|
||||
for (const auto& child : node->children) {
|
||||
if (child->element_type == ElementType::OPTION) {
|
||||
std::string option_value = child->value.empty() ? child->get_all_text() : child->value;
|
||||
std::string option_text = child->get_all_text();
|
||||
node->options.push_back({option_value, option_text});
|
||||
|
||||
// Set selected option if marked
|
||||
if (child->checked) {
|
||||
node->selected_option = node->options.size() - 1;
|
||||
node->value = option_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set default value to first option if no option is selected
|
||||
if (!node->options.empty() && node->value.empty()) {
|
||||
node->value = node->options[0].first;
|
||||
node->selected_option = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset form ID if we are exiting a form
|
||||
if (element.tag == GUMBO_TAG_FORM) {
|
||||
g_current_form_id = -1; // Assuming no nested forms
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ struct DomNode {
|
|||
bool checked = false;
|
||||
int form_id = -1;
|
||||
|
||||
// SELECT元素的选项
|
||||
std::vector<std::pair<std::string, std::string>> options; // (value, text) pairs
|
||||
int selected_option = 0; // 当前选中的选项索引
|
||||
|
||||
// 辅助方法
|
||||
bool is_block_element() const;
|
||||
bool is_inline_element() const;
|
||||
|
|
|
|||
|
|
@ -386,6 +386,32 @@ public:
|
|||
return result;
|
||||
}
|
||||
|
||||
InputResult process_select_option_mode(int ch) {
|
||||
InputResult result;
|
||||
result.action = Action::NONE;
|
||||
|
||||
if (ch == 27) {
|
||||
// ESC cancels selection
|
||||
mode = InputMode::NORMAL;
|
||||
return result;
|
||||
} else if (ch == '\n' || ch == '\r') {
|
||||
// Enter selects current option
|
||||
result.action = Action::SELECT_CURRENT_OPTION;
|
||||
mode = InputMode::NORMAL;
|
||||
return result;
|
||||
} else if (ch == 'j' || ch == KEY_DOWN) {
|
||||
// Next option
|
||||
result.action = Action::NEXT_OPTION;
|
||||
return result;
|
||||
} else if (ch == 'k' || ch == KEY_UP) {
|
||||
// Previous option
|
||||
result.action = Action::PREV_OPTION;
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
InputHandler::InputHandler() : pImpl(std::make_unique<Impl>()) {}
|
||||
|
|
@ -406,6 +432,8 @@ InputResult InputHandler::handle_key(int ch) {
|
|||
return pImpl->process_link_hints_mode(ch);
|
||||
case InputMode::FORM_EDIT:
|
||||
return pImpl->process_form_edit_mode(ch);
|
||||
case InputMode::SELECT_OPTION:
|
||||
return pImpl->process_select_option_mode(ch);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ enum class InputMode {
|
|||
COMMAND,
|
||||
SEARCH,
|
||||
LINK,
|
||||
LINK_HINTS, // Vimium-style 'f' mode
|
||||
FORM_EDIT // Form field editing mode
|
||||
LINK_HINTS, // Vimium-style 'f' mode
|
||||
FORM_EDIT, // Form field editing mode
|
||||
SELECT_OPTION // Dropdown selection mode
|
||||
};
|
||||
|
||||
enum class Action {
|
||||
|
|
@ -49,7 +50,10 @@ enum class Action {
|
|||
ACTIVATE_FIELD, // Activate current field for editing (Enter)
|
||||
TOGGLE_CHECKBOX, // Toggle checkbox state
|
||||
EDIT_TEXT, // Edit text input (updates text buffer)
|
||||
SUBMIT_FORM // Submit form (Enter on submit button)
|
||||
SUBMIT_FORM, // Submit form (Enter on submit button)
|
||||
NEXT_OPTION, // Move to next dropdown option (j/down)
|
||||
PREV_OPTION, // Move to previous dropdown option (k/up)
|
||||
SELECT_CURRENT_OPTION // Select current dropdown option (Enter)
|
||||
};
|
||||
|
||||
struct InputResult {
|
||||
|
|
|
|||
|
|
@ -432,9 +432,13 @@ void LayoutEngine::layout_form_element(const DomNode* node, Context& /*ctx*/, st
|
|||
span.field_index = node->field_index;
|
||||
line.spans.push_back(span);
|
||||
} else if (node->element_type == ElementType::SELECT) {
|
||||
// 下拉选择
|
||||
// 下拉选择 - 显示当前选中的选项
|
||||
StyledSpan span;
|
||||
span.text = "[▼ Select]";
|
||||
std::string selected_text = "Select";
|
||||
if (node->selected_option >= 0 && node->selected_option < static_cast<int>(node->options.size())) {
|
||||
selected_text = node->options[node->selected_option].second;
|
||||
}
|
||||
span.text = "[▼ " + selected_text + "]";
|
||||
span.fg = colors::INPUT_FOCUS;
|
||||
span.bg = colors::INPUT_BG;
|
||||
span.field_index = node->field_index;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,21 @@
|
|||
<p>Email: <input type="text" name="email" placeholder="email@example.com"></p>
|
||||
<p>Password: <input type="password" name="password"></p>
|
||||
|
||||
<p>Country: <select name="country">
|
||||
<option value="">Select a country</option>
|
||||
<option value="us">United States</option>
|
||||
<option value="uk">United Kingdom</option>
|
||||
<option value="ca">Canada</option>
|
||||
<option value="au">Australia</option>
|
||||
</select></p>
|
||||
|
||||
<p>Age Group: <select name="age">
|
||||
<option value="18-25" selected>18-25</option>
|
||||
<option value="26-35">26-35</option>
|
||||
<option value="36-50">36-50</option>
|
||||
<option value="51+">51+</option>
|
||||
</select></p>
|
||||
|
||||
<p><input type="checkbox" name="subscribe"> Subscribe to newsletter</p>
|
||||
<p><input type="checkbox" name="agree"> I agree to terms</p>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue