diff --git a/nomadnet/examples/various/input_fields.py b/nomadnet/examples/various/input_fields.py index 43d7e65..5d1eccc 100644 --- a/nomadnet/examples/various/input_fields.py +++ b/nomadnet/examples/various/input_fields.py @@ -17,6 +17,8 @@ -= + +>>>Text Fields An input field : `B444``b An masked field : `B444``b @@ -27,7 +29,24 @@ The data can be `!`[submitted`:/page/input_fields.mu`username|two]`!. -You can `!`[submit`:/page/input_fields.mu`one|password|small]`! other fields, or just `!`[a single one`:/page/input_fields.mu`username]`! +>> Checkbox Fields + +`B444``b Sign me up + +>> Radio group + +Select your favorite color: + +`B900`<^|color|Red`>`b Red + +`B090`<^|color|Green`>`b Green + +`B009`<^|color|Blue`>`b Blue + + +>>> Submitting data + +You can `!`[submit`:/page/input_fields.mu`one|password|small|color]`! other fields, or just `!`[a single one`:/page/input_fields.mu`username]`! Or simply `!`[submit them all`:/page/input_fields.mu`*]`!. diff --git a/nomadnet/ui/textui/Browser.py b/nomadnet/ui/textui/Browser.py index 58ae008..5e412a4 100644 --- a/nomadnet/ui/textui/Browser.py +++ b/nomadnet/ui/textui/Browser.py @@ -180,23 +180,47 @@ def handle_link(self, link_target, link_data = None): else: link_fields.append(e) - def recurse_down(w): - target = None - if isinstance(w, list): - for t in w: - recurse_down(t) - elif isinstance(w, tuple): - for t in w: - recurse_down(t) - elif hasattr(w, "contents"): - recurse_down(w.contents) - elif hasattr(w, "original_widget"): - recurse_down(w.original_widget) - elif hasattr(w, "_original_widget"): - recurse_down(w._original_widget) - else: - if hasattr(w, "field_name") and (all_fields or w.field_name in link_fields): - request_data["field_"+w.field_name] = w.get_edit_text() + def recurse_down(w): + if isinstance(w, list): + for t in w: + recurse_down(t) + elif isinstance(w, tuple): + for t in w: + recurse_down(t) + elif hasattr(w, "contents"): + recurse_down(w.contents) + elif hasattr(w, "original_widget"): + recurse_down(w.original_widget) + elif hasattr(w, "_original_widget"): + recurse_down(w._original_widget) + else: + if hasattr(w, "field_name") and (all_fields or w.field_name in link_fields): + field_key = "field_" + w.field_name + if isinstance(w, urwid.Edit): + request_data[field_key] = w.edit_text + elif isinstance(w, urwid.RadioButton): + if w.state: + user_data = getattr(w, "field_value", None) + if user_data is not None: + request_data[field_key] = user_data + elif isinstance(w, urwid.CheckBox): + user_data = getattr(w, "field_value", "1") + if w.state: + existing_value = request_data.get(field_key, '') + if existing_value: + # Concatenate the new value with the existing one + request_data[field_key] = existing_value + ',' + user_data + else: + # Initialize the field with the current value + request_data[field_key] = user_data + else: + pass # do nothing if checkbox is not check + + + + + + recurse_down(self.attr_maps) RNS.log("Including request data: "+str(request_data), RNS.LOG_DEBUG) diff --git a/nomadnet/ui/textui/Guide.py b/nomadnet/ui/textui/Guide.py index 69305cf..ba6c4d5 100644 --- a/nomadnet/ui/textui/Guide.py +++ b/nomadnet/ui/textui/Guide.py @@ -1152,6 +1152,47 @@ def focus_reader(self): A masked input field: `B444``B333 Full control: `B444``B333 +`b +>>> Checkboxes + +In addition to text fields, Checkboxes are another way of submitting data. They allow the user to make a single selection or select multiple options. + +`Faaa +`= +``b Label Text` +`= +When the checkbox is checked, it's field will be set to the provided value. If there are multiple checkboxes that share the same field name, the checked values will be concatenated when they are sent to the node by a comma. +`` + +`B444``b Sign me up` + +You can also pre-check both checkboxes and radio groups by appending a |* after the field value. + +`B444``b Pre-checked checkbox` + +>>> Radio groups + +Radio groups are another input that lets the user chose from a set of options. Unlike checkboxes, radio buttons with the same field name are mutually exclusive. + +Example: + +`= +`B900`<^|color|Red`>`b Red + +`B090`<^|color|Green`>`b Green + +`B009`<^|color|Blue`>`b Blue +`= + +will render: + +`B900`<^|color|Red`>`b Red + +`B090`<^|color|Green`>`b Green + +`B009`<^|color|Blue`>`b Blue + +In this example, when the data is submitted, `B444` field_color`b will be set to whichever value from the list was selected. `` diff --git a/nomadnet/ui/textui/MicronParser.py b/nomadnet/ui/textui/MicronParser.py index 27e420b..05ded7a 100644 --- a/nomadnet/ui/textui/MicronParser.py +++ b/nomadnet/ui/textui/MicronParser.py @@ -160,7 +160,6 @@ def parse_line(line, state, url_delegate): tw.in_columns = True else: tw = urwid.Text(o, align=state["align"]) - widgets.append((urwid.PACK, tw)) else: if o["type"] == "field": @@ -169,10 +168,40 @@ def parse_line(line, state, url_delegate): fn = o["name"] fs = o["style"] fmask = "*" if o["masked"] else None - f = urwid.Edit(caption="", edit_text=fd, align=state["align"], multiline=True, mask=fmask) + f = urwid.Edit(caption="", edit_text=fd, align=state["align"], multiline=False, mask=fmask) f.field_name = fn fa = urwid.AttrMap(f, fs) widgets.append((fw, fa)) + elif o["type"] == "checkbox": + fn = o["name"] + fv = o["value"] + flabel = o["label"] + fs = o["style"] + fprechecked = o.get("prechecked", False) + f = urwid.CheckBox(flabel, state=fprechecked) + f.field_name = fn + f.field_value = fv + fa = urwid.AttrMap(f, fs) + widgets.append((urwid.PACK, fa)) + elif o["type"] == "radio": + fn = o["name"] + fv = o["value"] + flabel = o["label"] + fs = o["style"] + fprechecked = o.get("prechecked", False) + if "radio_groups" not in state: + state["radio_groups"] = {} + if fn not in state["radio_groups"]: + state["radio_groups"][fn] = [] + group = state["radio_groups"][fn] + f = urwid.RadioButton(group, flabel, state=fprechecked, user_data=fv) + f.field_name = fn + f.field_value = fv + fa = urwid.AttrMap(f, fs) + widgets.append((urwid.PACK, fa)) + + + columns_widget = urwid.Columns(widgets, dividechars=0) text_widget = columns_widget @@ -458,54 +487,100 @@ def make_output(state, line, url_delegate): elif c == "a": state["align"] = state["default_align"] - elif c == "<": + elif c == '<': + if len(part) > 0: + output.append(make_part(state, part)) + part = "" try: - field_name = None - field_name_end = line[i:].find("`") - if field_name_end == -1: - pass + field_start = i + 1 # position after '<' + backtick_pos = line.find('`', field_start) + if backtick_pos == -1: + pass # No '`', invalid field else: - field_name = line[i+1:i+field_name_end] - field_name_skip = len(field_name) + field_content = line[field_start:backtick_pos] field_masked = False field_width = 24 - - if "|" in field_name: - f_components = field_name.split("|") + field_type = "field" + field_name = field_content + field_value = "" + field_data = "" + field_prechecked = False + + # check if field_content contains '|' + if '|' in field_content: + f_components = field_content.split('|') field_flags = f_components[0] field_name = f_components[1] - if "!" in field_flags: + + # handle field type indicators + if '^' in field_flags: + field_type = "radio" + field_flags = field_flags.replace("^", "") + elif '?' in field_flags: + field_type = "checkbox" + field_flags = field_flags.replace("?", "") + elif '!' in field_flags: field_flags = field_flags.replace("!", "") field_masked = True + + # Handle field width if len(field_flags) > 0: - field_width = min(int(field_flags), 256) - - def sr(): - return "@{"+str(random.randint(1000,9999))+"}" - rsg = sr() - while rsg in line[i+field_name_end:]: - rsg = sr() - lr = line[i+field_name_end:].replace("\\>", rsg) - endpos = lr.find(">") - - if endpos == -1: - pass - + try: + field_width = min(int(field_flags), 256) + except ValueError: + pass # Ignore invalid width + + # Check for value and pre-checked flag + if len(f_components) > 2: + field_value = f_components[2] + else: + field_value = "" + if len(f_components) > 3: + if f_components[3] == '*': + field_prechecked = True + + else: + # No '|', so field_name is field_content + field_name = field_content + field_type = "field" + field_masked = False + field_width = 24 + field_value = "" + field_prechecked = False + + # Find the closing '>' character + field_end = line.find('>', backtick_pos) + if field_end == -1: + pass # No closing '>', invalid field else: - field_data = lr[1:endpos].replace(rsg, "\\>") - skip = len(field_data)+field_name_skip+2 - field_data = field_data.replace("\\>", ">") - output.append({ - "type":"field", - "name": field_name, - "width": field_width, - "masked": field_masked, - "data": field_data, - "style": make_style(state) - }) + field_data = line[backtick_pos+1:field_end] + + # Now, we have all field data + if field_type in ["checkbox", "radio"]: + # for checkboxes and radios, field_data is the label + output.append({ + "type": field_type, + "name": field_name, + "value": field_value if field_value else field_data, + "label": field_data, + "prechecked": field_prechecked, + "style": make_style(state) + }) + else: + # For text fields field_data is the initial text + output.append({ + "type": "field", + "name": field_name, + "width": field_width, + "masked": field_masked, + "data": field_data, + "style": make_style(state) + }) + skip = field_end - i except Exception as e: pass - + + elif c == "[": endpos = line[i:].find("]") if endpos == -1: