From 7cd4a189e01d49dc85e7a672b283f02c4452faa4 Mon Sep 17 00:00:00 2001
From: lemoer <git@irrelefant.net>
Date: Fri, 15 Apr 2022 00:44:39 +0200
Subject: [PATCH] gluon-web-model: add MultiListValue class

---
 .luacheckrc                                   |  1 +
 .../gluon/config-mode/www/static/gluon.css    |  2 +-
 .../gluon-config-mode-theme/sass/gluon.scss   |  7 +++
 .../lib/gluon/web/view/model/mlvalue.html     | 23 ++++++++
 .../usr/lib/lua/gluon/web/model/classes.lua   | 54 +++++++++++++++++++
 5 files changed, 86 insertions(+), 1 deletion(-)
 create mode 100644 package/gluon-web-model/files/lib/gluon/web/view/model/mlvalue.html

diff --git a/.luacheckrc b/.luacheckrc
index 2f491ca25..0f17ecc56 100644
--- a/.luacheckrc
+++ b/.luacheckrc
@@ -50,6 +50,7 @@ files["package/**/check_site.lua"] = {
 
 files["package/**/luasrc/lib/gluon/config-mode/*"] = {
 	globals = {
+		"MultiListValue",
 		"DynamicList",
 		"Flag",
 		"Form",
diff --git a/package/gluon-config-mode-theme/files/lib/gluon/config-mode/www/static/gluon.css b/package/gluon-config-mode-theme/files/lib/gluon/config-mode/www/static/gluon.css
index c84ac01be..296dc49a3 100644
--- a/package/gluon-config-mode-theme/files/lib/gluon/config-mode/www/static/gluon.css
+++ b/package/gluon-config-mode-theme/files/lib/gluon/config-mode/www/static/gluon.css
@@ -1 +1 @@
-html{min-height:100%;height:auto;position:relative}body,input,select,option{font-family:'Open Sans', Arial, sans-serif;font-size:12pt}body{color:#4d4e53;line-height:1.5em;margin:0;display:flex;flex-direction:column;min-height:100vh;background-color:#f3f3f3}.tabmenu1{text-align:center}ul.tabmenu{list-style:none;padding:0;margin:2em 0;display:inline-flex}ul.tabmenu li{white-space:nowrap;margin:0 0.5em;padding:0;text-align:center}ul.tabmenu li a{display:block;text-decoration:none;padding:1em;margin:0;color:#333;border-radius:2em}ul.tabmenu li a:hover{background:#ffe9b3}ul.tabmenu li.active a{font-weight:bold;background:white;color:#333}#maincontent ul{margin-left:2em}.error{color:#ff0000;background-color:white}#menubar{display:flex;background:#dc0067;color:#ffffff}#menubar a:link.topcat,#menubar a:visited.topcat{position:relative;display:block;padding:0.5em;text-decoration:none;font-size:80%;font-weight:normal;color:white}#menubar a:link.topcat:hover,#menubar a:link.topcat:focus,#menubar a:visited.topcat:hover,#menubar a:visited.topcat:focus{background:#ffb400;color:black}#menubar a:link.topcat.active,#menubar a:visited.topcat.active{background:#ffb400;color:black;font-weight:bold}#menubar .hostinfo{position:relative;margin:0;padding:0.5em;flex:1;font-weight:bold;font-size:80%}#menubar .hostinfo a:link,#menubar .hostinfo a:visited{text-decoration:none;font-weight:bold;color:white}#menubar .hostinfo a:link:hover,#menubar .hostinfo a:link:focus,#menubar .hostinfo a:visited:hover,#menubar .hostinfo a:visited:focus{text-decoration:underline}#topmenu{list-style:none;margin:0;padding:0}#topmenu li{display:inline-block}#maincontent{padding:0 1em 2em;max-width:60em;min-width:40em;margin:1em auto}#maincontent p{margin-bottom:1em}.gluon-section{margin:0;padding:0;border:none;margin-bottom:1.3em}.gluon-section:last-child{margin-bottom:0.7em}.gluon-section legend{font-size:1.4em;font-weight:bold;position:relative;padding:0;margin-bottom:0.5em}.gluon-section h2{margin:0em 0 0.5em -0.5em}.gluon-section h3{text-decoration:none;font-weight:bold;color:#555555;margin:0.25em;font-size:100%}.gluon-section-descr,.gluon-warning{margin-bottom:2em}.gluon-osm-map{width:100%;height:40em;margin-bottom:1em}input::placeholder{color:#aaaaaa}input::-webkit-input-placeholder{color:#aaaaaa}input[type=checkbox]{display:none}input[type=checkbox]+label{display:inline-block;position:relative;width:1em;height:1em;margin:0}input[type=checkbox]:checked+label::after{content:'✔';color:#dc0067;vertical-align:middle;position:absolute;top:50%;left:0;margin-top:-0.5em;width:100%;text-align:center;font-size:1.7em}input[type=radio]{display:none}input[type=radio]+label{display:inline-block;position:relative;width:0.8em;height:0.8em;padding:0.5em;margin:0.2em 0.2em 0.2em 0.1em;border:none;background:#ffe199;vertical-align:middle;border-radius:50%}input[type=radio]:checked+label::after{content:'•';color:#dc0067;vertical-align:middle;position:absolute;top:50%;left:0;margin-top:-0.4em;width:100%;text-align:center;font-size:2em}input[type=submit],input[type=reset],input[type=button]{cursor:pointer}select,input,textarea,input[type=checkbox]+label{color:#003247;border:none;background:#ffe199;border-radius:3pt;padding:0.5em;margin-top:1px;margin-bottom:2px;box-sizing:content-box;outline:0}.select-wrapper{position:relative;display:inline-block}.select-wrapper::before{position:absolute;z-index:1;right:0.05em;top:calc(2px + 0.1em);bottom:calc(2px + 0.1em);width:1.4em;border-left:0.05em solid rgba(0,0,0,0.25);pointer-events:none;background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="8" height="5"><path fill="none" stroke="black" stroke-linejoin="bevel" d="M1,1L4,4L7,1"/></svg>') center/0.8em 0.5em no-repeat;content:''}.select-wrapper select{-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer}option{color:#003247;background:#ffe199}select,input[type=text],input[type=password]{min-width:20em}.gluon-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:baseline;text-align:center;cursor:pointer;user-select:none;font-size:100%;padding:0.5em 1em;color:rgba(0,0,0,0.8);background-color:#E6E6E6;border:none;text-decoration:none;border-radius:2px;transition:0.1s linear box-shadow;margin-left:0.5em;background-repeat:no-repeat}.gluon-button::-moz-focus-inner{padding:0;border:0}.gluon-button:active{box-shadow:0 0 0 1px rgba(0,0,0,0.15) inset,0 0 6px rgba(0,0,0,0.2) inset}.gluon-button:focus{outline:0}.gluon-button:hover,.gluon-button:focus{background-image:linear-gradient(transparent, rgba(0,0,0,0.05) 40%, rgba(0,0,0,0.1))}.gluon-button[disabled]{border:none;background-image:none;opacity:0.40;cursor:not-allowed;box-shadow:none}.gluon-button-reset{background-color:#e30;color:#fff}.gluon-button-submit{background-color:#009ee0;color:#fff}.gluon-button-submit:active{background:grey}.gluon-input-invalid{background:#e30 !important;color:white}textarea{margin-left:-1px;margin-bottom:0.5em}.gluon-value{display:flex;flex-direction:row;margin-bottom:0.5em}.gluon-section-node .gluon-value:last-child{margin-bottom:0}.gluon-value-title{flex:2;text-align:right;padding-top:0.39em;padding-right:1em;font-weight:bold}.gluon-value-field{flex:3;position:relative}.gluon-value-field input,.gluon-value-field select,.gluon-value-field input+label{position:relative}.gluon-value-field-text{flex:3;padding-top:0.39em}.gluon-value-field-long{flex:10;position:relative;margin-top:0.65em}.gluon-value-field-long input,.gluon-value-field-long select,.gluon-value-field-long input+label{position:relative}.gluon-value-field-long-after{flex:2}.gluon-value-description{font-size:8pt}.gluon-form-descr{margin-bottom:1em}.gluon-form-descr:empty,.gluon-section-descr:empty,.gluon-warning:empty{display:none}.gluon-form-descr,.gluon-section-descr,.gluon-warning,.gluon-page-actions{padding:1em;background:#ececec}.gluon-page-actions{text-align:right;display:flex;flex-flow:row-reverse}.gluon-section-node{clear:both;position:relative;border:none}.gluon-value-error input,.gluon-value-error select{background-color:#ffcccc}.gluon-add::after,.gluon-remove::after{cursor:pointer;display:inline-block;text-align:center;vertical-align:middle;font-size:180%;width:1.2em;height:1em}.gluon-add{color:#008000;position:relative;left:21em}input+.gluon-add{left:0;top:0.04em}.gluon-add:first-child{top:0.53em;left:-0.08em}.gluon-add::after{content:'+'}.gluon-remove{color:#800000;position:relative;top:-0.03em}.gluon-remove::after{content:'–'}.gluon-warning{background:#ffe9b3}.error500{border:1px dotted #ff0000;background-color:#ffffff;color:#000000;padding:0.5em}.errorbox{border:1px solid #FF0000;background-color:#FFCCCC;padding:5px;margin-bottom:5px}.errorbox a{color:#000000 !important}.the-key{text-align:left;font-size:1.4em;background:#ffe9b3;border:3pt dashed #dc0067;margin-bottom:0.5em;padding:0.5em}
+html{min-height:100%;height:auto;position:relative}body,input,select,option{font-family:'Open Sans', Arial, sans-serif;font-size:12pt}body{color:#4d4e53;line-height:1.5em;margin:0;display:flex;flex-direction:column;min-height:100vh;background-color:#f3f3f3}.tabmenu1{text-align:center}ul.tabmenu{list-style:none;padding:0;margin:2em 0;display:inline-flex}ul.tabmenu li{white-space:nowrap;margin:0 0.5em;padding:0;text-align:center}ul.tabmenu li a{display:block;text-decoration:none;padding:1em;margin:0;color:#333;border-radius:2em}ul.tabmenu li a:hover{background:#ffe9b3}ul.tabmenu li.active a{font-weight:bold;background:white;color:#333}#maincontent ul{margin-left:2em}.error{color:#ff0000;background-color:white}#menubar{display:flex;background:#dc0067;color:#ffffff}#menubar a:link.topcat,#menubar a:visited.topcat{position:relative;display:block;padding:0.5em;text-decoration:none;font-size:80%;font-weight:normal;color:white}#menubar a:link.topcat:hover,#menubar a:link.topcat:focus,#menubar a:visited.topcat:hover,#menubar a:visited.topcat:focus{background:#ffb400;color:black}#menubar a:link.topcat.active,#menubar a:visited.topcat.active{background:#ffb400;color:black;font-weight:bold}#menubar .hostinfo{position:relative;margin:0;padding:0.5em;flex:1;font-weight:bold;font-size:80%}#menubar .hostinfo a:link,#menubar .hostinfo a:visited{text-decoration:none;font-weight:bold;color:white}#menubar .hostinfo a:link:hover,#menubar .hostinfo a:link:focus,#menubar .hostinfo a:visited:hover,#menubar .hostinfo a:visited:focus{text-decoration:underline}#topmenu{list-style:none;margin:0;padding:0}#topmenu li{display:inline-block}#maincontent{padding:0 1em 2em;max-width:60em;min-width:40em;margin:1em auto}#maincontent p{margin-bottom:1em}.gluon-section{margin:0;padding:0;border:none;margin-bottom:1.3em}.gluon-section:last-child{margin-bottom:0.7em}.gluon-section legend{font-size:1.4em;font-weight:bold;position:relative;padding:0;margin-bottom:0.5em}.gluon-section h2{margin:0em 0 0.5em -0.5em}.gluon-section h3{text-decoration:none;font-weight:bold;color:#555555;margin:0.25em;font-size:100%}.gluon-section-descr,.gluon-warning{margin-bottom:2em}.gluon-osm-map{width:100%;height:40em;margin-bottom:1em}input::placeholder{color:#aaaaaa}input::-webkit-input-placeholder{color:#aaaaaa}input[type=checkbox]{display:none}input[type=checkbox]+label{display:inline-block;position:relative;width:1em;height:1em;margin:0}input[type=checkbox]:checked+label::after{content:'✔';color:#dc0067;vertical-align:middle;position:absolute;top:50%;left:0;margin-top:-0.5em;width:100%;text-align:center;font-size:1.7em}input[type=radio]{display:none}input[type=radio]+label{display:inline-block;position:relative;width:0.8em;height:0.8em;padding:0.5em;margin:0.2em 0.2em 0.2em 0.1em;border:none;background:#ffe199;vertical-align:middle;border-radius:50%}input[type=radio]:checked+label::after{content:'•';color:#dc0067;vertical-align:middle;position:absolute;top:50%;left:0;margin-top:-0.4em;width:100%;text-align:center;font-size:2em}input[type=submit],input[type=reset],input[type=button]{cursor:pointer}select,input,textarea,input[type=checkbox]+label{color:#003247;border:none;background:#ffe199;border-radius:3pt;padding:0.5em;margin-top:1px;margin-bottom:2px;box-sizing:content-box;outline:0}.select-wrapper{position:relative;display:inline-block}.select-wrapper::before{position:absolute;z-index:1;right:0.05em;top:calc(2px + 0.1em);bottom:calc(2px + 0.1em);width:1.4em;border-left:0.05em solid rgba(0,0,0,0.25);pointer-events:none;background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="8" height="5"><path fill="none" stroke="black" stroke-linejoin="bevel" d="M1,1L4,4L7,1"/></svg>') center/0.8em 0.5em no-repeat;content:''}.select-wrapper select{-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer}option{color:#003247;background:#ffe199}select,input[type=text],input[type=password]{min-width:20em}.gluon-multi-list-option-descr{display:inline-block;vertical-align:top;margin-top:0.35em;margin-left:0.4em}.gluon-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:baseline;text-align:center;cursor:pointer;user-select:none;font-size:100%;padding:0.5em 1em;color:rgba(0,0,0,0.8);background-color:#E6E6E6;border:none;text-decoration:none;border-radius:2px;transition:0.1s linear box-shadow;margin-left:0.5em;background-repeat:no-repeat}.gluon-button::-moz-focus-inner{padding:0;border:0}.gluon-button:active{box-shadow:0 0 0 1px rgba(0,0,0,0.15) inset,0 0 6px rgba(0,0,0,0.2) inset}.gluon-button:focus{outline:0}.gluon-button:hover,.gluon-button:focus{background-image:linear-gradient(transparent, rgba(0,0,0,0.05) 40%, rgba(0,0,0,0.1))}.gluon-button[disabled]{border:none;background-image:none;opacity:0.40;cursor:not-allowed;box-shadow:none}.gluon-button-reset{background-color:#e30;color:#fff}.gluon-button-submit{background-color:#009ee0;color:#fff}.gluon-button-submit:active{background:grey}.gluon-input-invalid{background:#e30 !important;color:white}textarea{margin-left:-1px;margin-bottom:0.5em}.gluon-value{display:flex;flex-direction:row;margin-bottom:0.5em}.gluon-section-node .gluon-value:last-child{margin-bottom:0}.gluon-value-title{flex:2;text-align:right;padding-top:0.39em;padding-right:1em;font-weight:bold}.gluon-value-field{flex:3;position:relative}.gluon-value-field input,.gluon-value-field select,.gluon-value-field input+label{position:relative}.gluon-value-field-text{flex:3;padding-top:0.39em}.gluon-value-field-long{flex:10;position:relative;margin-top:0.65em}.gluon-value-field-long input,.gluon-value-field-long select,.gluon-value-field-long input+label{position:relative}.gluon-value-field-long-after{flex:2}.gluon-value-description{font-size:8pt}.gluon-form-descr{margin-bottom:1em}.gluon-form-descr:empty,.gluon-section-descr:empty,.gluon-warning:empty{display:none}.gluon-form-descr,.gluon-section-descr,.gluon-warning,.gluon-page-actions{padding:1em;background:#ececec}.gluon-page-actions{text-align:right;display:flex;flex-flow:row-reverse}.gluon-section-node{clear:both;position:relative;border:none}.gluon-value-error input,.gluon-value-error select{background-color:#ffcccc}.gluon-add::after,.gluon-remove::after{cursor:pointer;display:inline-block;text-align:center;vertical-align:middle;font-size:180%;width:1.2em;height:1em}.gluon-add{color:#008000;position:relative;left:21em}input+.gluon-add{left:0;top:0.04em}.gluon-add:first-child{top:0.53em;left:-0.08em}.gluon-add::after{content:'+'}.gluon-remove{color:#800000;position:relative;top:-0.03em}.gluon-remove::after{content:'–'}.gluon-warning{background:#ffe9b3}.error500{border:1px dotted #ff0000;background-color:#ffffff;color:#000000;padding:0.5em}.errorbox{border:1px solid #FF0000;background-color:#FFCCCC;padding:5px;margin-bottom:5px}.errorbox a{color:#000000 !important}.the-key{text-align:left;font-size:1.4em;background:#ffe9b3;border:3pt dashed #dc0067;margin-bottom:0.5em;padding:0.5em}
diff --git a/package/gluon-config-mode-theme/sass/gluon.scss b/package/gluon-config-mode-theme/sass/gluon.scss
index 575af4f92..3af8d5e00 100644
--- a/package/gluon-config-mode-theme/sass/gluon.scss
+++ b/package/gluon-config-mode-theme/sass/gluon.scss
@@ -366,6 +366,13 @@ input[type=password] {
 	min-width: 20em;
 }
 
+.gluon-multi-list-option-descr {
+	display: inline-block;
+	vertical-align: top;
+	margin-top: 0.35em;
+	margin-left: 0.4em;
+}
+
 .gluon-button {
 	@include button;
 
diff --git a/package/gluon-web-model/files/lib/gluon/web/view/model/mlvalue.html b/package/gluon-web-model/files/lib/gluon/web/view/model/mlvalue.html
new file mode 100644
index 000000000..a65eda744
--- /dev/null
+++ b/package/gluon-web-model/files/lib/gluon/web/view/model/mlvalue.html
@@ -0,0 +1,23 @@
+<%
+	local br = self.orientation == "horizontal" and '&#160;&#160;&#160;' or '<br>'
+	local entries = self:entries()
+	local util = require 'gluon.util'
+%>
+<div>
+	<% for i, entry in pairs(entries) do %>
+		<label<%=
+			attr("data-index", i) ..
+			attr("data-depends", self:deplist(entry.deps))
+		%>>
+			<input data-update="click change" type="checkbox"<%=
+				attr("id", id.."."..entry.key) ..
+				attr("name", id) ..
+				attr("value", entry.key) ..
+				attr("checked", (util.contains(self:cfgvalue(), entry.key)) and "checked")
+			%>>
+			<label<%= attr("for", id.."."..entry.key)%>></label>
+			<span class="gluon-multi-list-option-descr"><%|entry.value%></span>
+		</label>
+		<% if i ~= #entries then write(br) end %>
+	<% end %>
+</div>
diff --git a/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model/classes.lua b/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model/classes.lua
index 93279c3b6..1498687a5 100644
--- a/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model/classes.lua
+++ b/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model/classes.lua
@@ -361,6 +361,60 @@ function ListValue:validate()
 end
 
 
+local MultiListValue = class(AbstractValue)
+M.MultiListValue = MultiListValue
+
+function MultiListValue:__init__(...)
+	AbstractValue.__init__(self, ...)
+	self.subtemplate  = "model/mlvalue"
+
+	self.size = 1
+
+	self.keys = {}
+	self.entry_list = {}
+end
+
+function MultiListValue:value(key, val, ...)
+	key = tostring(key)
+
+	if self.keys[key] then
+		return
+	end
+	self.keys[key] = true
+
+	val = val or key
+	table.insert(self.entry_list, {
+		key = key,
+		value = tostring(val),
+		deps = {...},
+	})
+end
+
+function MultiListValue:entries()
+	local ret = {unpack(self.entry_list)}
+
+	return ret
+end
+
+function MultiListValue:validate()
+	for _, val in ipairs(self.data) do
+		if not self.keys[val] then
+			return false
+		end
+	end
+
+	return true
+end
+
+function MultiListValue:defaultvalue()
+	return self.default or {}
+end
+
+function MultiListValue:formvalue(http)
+	return http:formvaluetable(self:id())
+end
+
+
 local DynamicList = class(AbstractValue)
 M.DynamicList = DynamicList
 
-- 
GitLab