From ff5e90982ea9d67a4ab7514a9814a06ac1426e0f Mon Sep 17 00:00:00 2001
From: ezsh <ezsh.junk@gmail.com>
Date: Sat, 4 Apr 2020 18:17:42 +0200
Subject: [PATCH] Add simple DOM options widget

Uses the same CSS classes as the <<options>> and creates a similar DOM
tree. However, unline the macro, executes a callback on option change.
---
 devTools/FC.d.ts                 | 10 +++-
 js/002-config/fc-js-init.js      | 83 ++++++++++++++++----------------
 src/004-base/arcologyBuilding.js |  2 +-
 src/arcologyBuilding/shops.js    |  2 +-
 src/gui/multipleInspect.js       |  2 +-
 src/js/main.js                   |  2 +-
 src/js/utilsDOM.js               | 38 +++++++++++++--
 7 files changed, 88 insertions(+), 51 deletions(-)

diff --git a/devTools/FC.d.ts b/devTools/FC.d.ts
index 2530ad10fbd..abc179ce417 100644
--- a/devTools/FC.d.ts
+++ b/devTools/FC.d.ts
@@ -249,8 +249,14 @@ declare namespace App {
 	namespace SF {}
 
 	namespace UI {
-		namespace DOM {}
-		namespace View {}
+		namespace DOM {
+			namespace Widgets { }
+		}
+		namespace View { }
+		namespace SlaveSummary {
+			type StringRenderer = (slave: App.Entity.SlaveState) => String;
+			type AppendRenderer = (slave: App.Entity.SlaveState, parentNode: Node) => void;
+		}
 	}
 
 	namespace Update {}
diff --git a/js/002-config/fc-js-init.js b/js/002-config/fc-js-init.js
index 784f7a7410c..6ee8fcd9812 100644
--- a/js/002-config/fc-js-init.js
+++ b/js/002-config/fc-js-init.js
@@ -4,59 +4,60 @@
 
 var App = { };
 
-App.Arcology = {
-	Cell: {},
-};
+// When adding namespace declarations, please consider needs of those using VSCode:
+// when you declare App.A{ A1:{}, A2:{} }, VSCode considers A, A1, and A2 to be
+// sealed namespaces and no new members are added to them with nested declaration later on.
+// This breaks code completion completely. Please instead declare them as:
+// App.A = {}; App.A.A1 = {}; App.A.A2 = {}. Thank you.
+
+// Also, such declaration basically required only for namespaces that span more than a single file.
+
+App.Arcology = {};
+App.Arcology.Cell = {},
 App.Art = {};
 App.Corporate = {};
-App.Data = {
-	Weather: {},
-	HeroSlaves: {},
-};
+App.Data = {};
+App.Data.HeroSlaves = { };
+App.Data.Weather = {};
 App.Debug = {};
 App.Desc = {};
-App.Encyclopedia = {
-	Entries: {}
-};
-App.Entity = {
-	Utils: {}
-};
+App.Encyclopedia = {};
+App.Encyclopedia.Entries = {};
+App.Entity = {};
+App.Entity.Utils = {};
 App.Events = {};
-App.Facilities = {
-	Arcade: {},
-	Brothel: {},
-	Cellblock: {},
-	Clinic: {},
-	Club: {},
-	Dairy: {},
-	Farmyard: {},
-	HGSuite: {},
-	MasterSuite: {},
-	Nursery: {},
-	Schoolroom: {},
-	ServantsQuarters: {},
-	Spa: {}
-};
+App.Facilities = {};
+App.Facilities.Arcade = {};
+App.Facilities.Brothel = {};
+App.Facilities.Cellblock = {};
+App.Facilities.Clinic = {};
+App.Facilities.Club = {}
+App.Facilities.Dairy = {};
+App.Facilities.Farmyard = {};
+App.Facilities.HGSuite = {};
+App.Facilities.MasterSuite = {};
+App.Facilities.Nursery = {};
+App.Facilities.Schoolroom = {};
+App.Facilities.ServantsQuarters = {};
+App.Facilities.Spa = {}
 App.Interact = {};
 App.Intro = {};
 App.MainView = {};
-App.Medicine = {
-	Modification: {},
-	OrganFarm: {
-		Organs: {}
-	},
-	Surgery: {}
-};
+App.Medicine = {};
+App.Medicine.Modification = {};
+App.Medicine.OrganFarm = {};
+App.Medicine.OrganFarm.Organs = {};
+App.Medicine.Surgery = {};
 App.RA = {};
 App.Reminders = {};
 App.SF = {};
 App.SecExp = {};
-App.UI = {
-	Budget: {},
-	DOM: {},
-	SlaveInteract: {},
-	View: {}
-};
+App.UI = {};
+App.UI.Budget = {};
+App.UI.DOM = {};
+App.UI.DOM.Widgets = {};
+App.UI.SlaveInteract = {};
+App.UI.View = {};
 App.Update = {};
 App.Utils = {};
 
diff --git a/src/004-base/arcologyBuilding.js b/src/004-base/arcologyBuilding.js
index 2900eae721e..9e33f19ae7f 100644
--- a/src/004-base/arcologyBuilding.js
+++ b/src/004-base/arcologyBuilding.js
@@ -217,7 +217,7 @@ App.Arcology.Cell.BaseCell = class extends App.Entity.Serializable {
 		if (cost > 0 || note === undefined) {
 			note = ` Costs ${cashFormat(cost)}${note !== undefined ? ` ${note}` : ""}.`;
 		}
-		App.UI.DOM.appendNewElement("span", note, div, "detail");
+		App.UI.DOM.appendNewElement("span", div, note, "detail");
 
 		if (domNote !== undefined) {
 			div.append(domNote); // this only exists for the farmyard, remove once that is out of alpha
diff --git a/src/arcologyBuilding/shops.js b/src/arcologyBuilding/shops.js
index 136879908be..96d6b106b78 100644
--- a/src/arcologyBuilding/shops.js
+++ b/src/arcologyBuilding/shops.js
@@ -301,7 +301,7 @@ App.Arcology.Cell.Shop = class extends App.Arcology.Cell.BaseCell {
 				}
 				break;
 			default:
-				App.UI.DOM.appendNewElement("span", `ERROR: bad shop type: ${this.type}`, fragment, "error");
+				App.UI.DOM.appendNewElement("span", fragment, `ERROR: bad shop type: ${this.type}`, "error");
 		}
 
 		if (this.owner === 1 && this.type === "Shops") {
diff --git a/src/gui/multipleInspect.js b/src/gui/multipleInspect.js
index 1abd7a86b0d..c46c8d3b712 100644
--- a/src/gui/multipleInspect.js
+++ b/src/gui/multipleInspect.js
@@ -24,7 +24,7 @@ App.UI.MultipleInspect = (function() {
 	 */
 	function MultipleInspectDOM(slaves, showFamilyTree, saleDescription, applyLaw) {
 		const frag = document.createDocumentFragment();
-		const tabbar = App.UI.DOM.appendNewElement("div", "", frag, "tabbar");
+		const tabbar = App.UI.DOM.appendNewElement("div", frag, "", "tabbar");
 
 		for (const slave of slaves) {
 			tabbar.append(App.UI.tabbar.tabButtonDOM(`slave${slave.ID}`, slave.slaveName));
diff --git a/src/js/main.js b/src/js/main.js
index 144c26aebee..983005dedc8 100644
--- a/src/js/main.js
+++ b/src/js/main.js
@@ -170,7 +170,7 @@ App.MainView.useGuard = function() {
 		return outerDiv;
 	}
 
-	App.UI.DOM.appendNewElement("span", App.Interact.guardPose(guard), outerDiv, "scene-intro");
+	App.UI.DOM.appendNewElement("span", outerDiv, App.Interact.guardPose(guard), "scene-intro");
 
 	function setEnvironment() {
 		V.activeSlave = guard;
diff --git a/src/js/utilsDOM.js b/src/js/utilsDOM.js
index 80194e7e9d9..a88f6d5fe84 100644
--- a/src/js/utilsDOM.js
+++ b/src/js/utilsDOM.js
@@ -121,7 +121,7 @@ App.UI.DOM.disabledLink = function(link, reasons) {
 
 /**
  * @param {string} tag - valid HTML tag
- * @param {string|Node} content
+ * @param {string|Node} [content]
  * @param {string|Array<string>} [classNames]
  * @returns {HTMLElement}
  */
@@ -134,18 +134,20 @@ App.UI.DOM.makeElement = function(tag, content, classNames) {
 			element.classList.add(classNames);
 		}
 	}
-	element.append(content);
+	if (content) {
+		element.append(content);
+	}
 	return element;
 };
 
 /**
  * @param {string} tag - valid HTML tag
- * @param {string|Node} content
  * @param {ParentNode} parent
+ * @param {string|Node} [content]
  * @param {string|Array<string>} [classNames]
  * @returns {HTMLElement}
  */
-App.UI.DOM.appendNewElement = function(tag, content, parent, classNames) {
+App.UI.DOM.appendNewElement = function(tag, parent, content, classNames) {
 	const element = App.UI.DOM.makeElement(tag, content, classNames);
 	parent.append(element);
 	return element;
@@ -287,6 +289,34 @@ App.UI.DOM.arrayToList = function(content, delimiter = ", ", lastDelimiter = " a
 	return fragment;
 };
 
+/**
+ * @param {function (*):void} callback
+ * @param {Object.<string, *>} options
+ * @param {*|function (): *} currentValue
+ * @param {string} [description]
+ * @returns {HTMLSpanElement}
+ */
+App.UI.DOM.Widgets.optionSelector = function(callback, options, currentValue, description) {
+	const res = document.createElement("span");
+	res.classList.add("optionMacro");
+	if (description) {
+		App.UI.DOM.appendNewElement("span", res, description, "optionDescription");
+	}
+	const curVal = (currentValue instanceof Function) ? currentValue() : currentValue;
+	const valueNode = App.UI.DOM.appendNewElement("span", res, null, "optionValue");
+	for (const s in options) {
+		const v = options[s];
+		const choice = App.UI.DOM.appendNewElement("span", valueNode, null, "optionMacroOption");
+		if (v === curVal) {
+			choice.classList.add("optionMacroSelected");
+			choice.innerText = s;
+		} else {
+			choice.append(App.UI.DOM.link(s, () => { callback(v); }));
+		}
+	}
+	return res;
+};
+
 /**
  * @param {string} text
  * @returns {HTMLElement}
-- 
GitLab