git » sdk » main » tree

[main] / borogove / Form.hx

package borogove;

import borogove.DataForm;

#if cpp
import HaxeCBridge;
#end

@:expose
#if cpp
@:build(HaxeSwiftBridge.expose())
#end
interface FormSection {
	public function title(): Null<String>;
	public function items(): Array<FormItem>;
}

@:expose
#if cpp
@:build(HaxeCBridge.expose())
@:build(HaxeSwiftBridge.expose())
#end
class FormItem {
	public final text: Null<String>;
	public final field: Null<FormField>;
	public final section: Null<FormSection>;
	public final status: Null<String>;
	public final tableHeader: Null<Array<FormField>>;
	@HaxeCBridge.noemit
	public final tableRows: Null<Array<Array<FormField>>>;

	@:allow(borogove)
	private function new(text: Null<String>, field: Null<FormField>, section: Null<FormSection>, tableHeader: Null<Array<FormField>> = null, tableRows: Null<Array<Array<FormField>>> = null, status: Null<String> = null) {
		this.text = text;
		this.field = field;
		this.section = section;
		this.tableHeader = tableHeader;
		this.tableRows = tableRows;
		this.status = status;
	}
}

#if cpp
@:build(HaxeCBridge.expose())
@:build(HaxeSwiftBridge.expose())
#end
class FormSubmitBuilder {
	private final data: Map<String, Array<String>> = [];

	public function new() { }

	public function add(k: String, v: String) {
		if (data.get(k) != null) {
			data.set(k, data.get(k).concat([v]));
		} else {
			data.set(k, [v]);
		}
	}

	@:allow(borogove)
	private function submit(form: Null<DataForm>) {
		final toSubmit = new Stanza("x", { xmlns: "jabber:x:data", type: "submit" });
		if (form != null) {
			for (f in form.fields) {
				if (data.get(f.name) == null && f.value.length > 0) {
					final tag = toSubmit.tag("field", { "var": f.name });
					for (v in f.value) {
						tag.textTag("value", v);
					}
					tag.up();
				} else if (f.required && (data.get(f.name) == null || data[f.name].length < 1)) {
					trace("No value provided for required field", f.name);
					return null;
				}
			}
		}
		for (k => vs in data) {
			final tag = toSubmit.tag("field", { "var": k });
			for (v in vs) {
				tag.textTag("value", v);
			}
			tag.up();
		}

		return toSubmit;
	}
}

typedef StringOrArray = haxe.extern.EitherType<String, Array<String>>;

@:expose
#if cpp
@:build(HaxeCBridge.expose())
@:build(HaxeSwiftBridge.expose())
#end
class Form implements FormSection {
	private final form: Null<DataForm>;
	private final oob: Null<OOB>;

	@:allow(borogove)
	private function new(form: Null<DataForm>, oob: Null<OOB>) {
		if (form == null && oob == null) throw "Need a form or OOB";
		this.form = form;
		this.oob = oob;
	}

	/**
		Is this form entirely results / read-only?
	**/
	public function isResult() {
		if (form == null) return true;

		return form.type == "result";
	}

	/**
		Title of this form
	**/
	public function title() {
		return form != null ? form.title : oob.desc;
	}

	/**
		URL to use instead of this form
	**/
	public function url() {
		return oob?.url;
	}

	/**
		Items to render inside this form
	**/
	public function items(): Array<FormItem> {
		if (form == null) return [];

		final s: Stanza = form;
		final hasLayout = s.getChild("page", "http://jabber.org/protocol/xdata-layout") != null;
		final items = [];
		for (child in s.allTags()) {
			if (child.name == "instructions" && (child.attr.get("xmlns") == null || child.attr.get("xmlns") == "jabber:x:data")) {
				items.push(new FormItem(child.getText(), null, null, null, null, child.attr.get("type")));
			}
			if (!hasLayout && child.name == "field" && (child.attr.get("xmlns") == null || child.attr.get("xmlns") == "jabber:x:data")) {
				final fld: Null<Field> = child;
				if (fld.type == "fixed" && fld.label == null) {
					for (v in fld.value) {
						items.push(new FormItem(v, null, null));
					}
				} else if (fld.type != "hidden") {
					items.push(new FormItem(null, fld, null));
				}
			}
			if (!hasLayout && child.name == "reported" && (child.attr.get("xmlns") == null || child.attr.get("xmlns") == "jabber:x:data")) {
				items.push(new FormItem(
					null, null, null,
					form.tableHeader?.map(f -> f.toFormField()),
					form.tableRows?.map(row -> row.map(f -> f.toFormField())) ?? []
				));
			}
			if (child.name == "page" && child.attr.get("xmlns") == "http://jabber.org/protocol/xdata-layout") {
				items.push(new FormItem(null, null, new FormLayoutSection(form, child)));
			}
		}

		return items;
	}

	#if js
	@:allow(borogove)
	private function submit(
		data: haxe.extern.EitherType<
			haxe.extern.EitherType<
				haxe.DynamicAccess<StringOrArray>,
				Map<String, StringOrArray>
			>,
			js.html.FormData
		>
	) {
		final builder = new FormSubmitBuilder();

		if (Std.isOfType(data, js.lib.Map)) {
			for (k => v in ((cast data) : Map<String, StringOrArray>)) {
				if (Std.isOfType(v, String)) {
					builder.add(k, v);
				} else {
					for (oneV in ((cast v) : Array<String>)) {
						builder.add(k, oneV);
					}
				}
			}
		#if !nodejs
		} else if (Std.isOfType(data, js.html.FormData)) {
			for (entry in new js.lib.HaxeIterator(((cast data) : js.html.FormData).entries())) {
				if (form.field(entry[0])?.type == "boolean") {
					// FormData may have booleans formatted like an HTML form
					builder.add(entry[0], entry[1] == "on" ? "true" : "false");
				} else {
					builder.add(entry[0], entry[1]);
				}
			}
		#end
		} else if (data != null) {
			for (k => v in ((cast data) : haxe.DynamicAccess<StringOrArray>)) {
				if (Std.isOfType(v, String)) {
					builder.add(k, v);
				} else {
					for (oneV in ((cast v) : Array<String>)) {
						builder.add(k, oneV);
					}
				}
			}
		}

		return builder.submit(form);
	}
	#else
	@:allow(borogove)
	private function submit(data: FormSubmitBuilder) {
		return data.submit(form);
	}
	#end
}

class FormLayoutSection implements FormSection {
	private final form: DataForm;
	private final section: Stanza;

	@:allow(borogove)
	private function new(form: DataForm, section: Stanza) {
		this.form = form;
		this.section = section;
	}

	public function title() {
		return section.attr.get("label");
	}

	public function items() {
		final items = [];
		for (child in section.allTags()) {
			if (child.name == "text" && (child.attr.get("xmlns") == null || child.attr.get("xmlns") == "http://jabber.org/protocol/xdata-layout")) {
				items.push(new FormItem(child.getText(), null, null));
			}
			if (child.name == "fieldref" && (child.attr.get("xmlns") == null || child.attr.get("xmlns") == "http://jabber.org/protocol/xdata-layout")) {
				items.push(new FormItem(null, form.field(child.attr.get("var")), null));
			}
			if (child.name == "reportedref" && (child.attr.get("xmlns") == null || child.attr.get("xmlns") == "http://jabber.org/protocol/xdata-layout")) {
				items.push(new FormItem(
					null, null, null,
					form.tableHeader?.map(f -> f.toFormField()),
					form.tableRows?.map(row -> row.map(f -> f.toFormField())) ?? []
				));
			}
			if (child.name == "section" && (child.attr.get("xmlns") == null || child.attr.get("xmlns") == "http://jabber.org/protocol/xdata-layout")) {
				items.push(new FormItem(null, null, new FormLayoutSection(form, child)));
			}
		}

		return items;
	}
}