diff --git a/docker/routlin-dash/app/view_page.py b/docker/routlin-dash/app/view_page.py
index bca5e68..434c0f5 100644
--- a/docker/routlin-dash/app/view_page.py
+++ b/docker/routlin-dash/app/view_page.py
@@ -1128,15 +1128,17 @@ def _render_field(item, tokens):
if input_type == 'number':
min_attr = f' min="{item["min"]}"' if 'min' in item else ''
max_attr = f' max="{item["max"]}"' if 'max' in item else ''
+ dyn_hint_html = '
'
inp = (f'')
+ f' class="form-input{extra_cls}"{readonly}'
+ f' data-validate="positive_int" />')
if item.get('layout') == 'inline':
return (f'')
return (f''
- f'{inp}{hint_html}
')
+ f'{inp}{dyn_hint_html}{hint_html}')
if input_type == 'textarea':
rows = item.get('rows', 4)
@@ -1878,6 +1880,17 @@ function classifyTime24h(s) {
return 'complete';
}
+function classifyPositiveInt(s, el) {
+ if (el && el.validity && el.validity.badInput) return 'invalid_char';
+ if (!s && s !== '0') return 'empty';
+ if (/[^0-9]/.test(s)) return 'invalid_char';
+ var n = parseInt(s, 10);
+ var min = (el && el.min !== '') ? parseInt(el.min, 10) : 0;
+ var max = (el && el.max !== '') ? parseInt(el.max, 10) : null;
+ if (n < min || (max !== null && n > max)) return 'out_of_range';
+ return 'complete';
+}
+
function classifySubnet(s) {
if (!s) return 'empty';
if (/[^0-9.]/.test(s)) return 'invalid_char';
@@ -2078,8 +2091,10 @@ document.addEventListener('click', function(e) {
if (inputType === 'checkbox') {
var checked = (val === true || val === 'true' || val === 1 || val === '1');
- td.innerHTML = '';
+ var cbLabel = fDef.checkbox_label ? ' ' + esc(fDef.checkbox_label) + '' : '';
+ td.innerHTML = '';
} else if (inputType === 'checkbox_multi') {
var opts = fDef.options || [];
var checked = [];
@@ -2106,7 +2121,9 @@ document.addEventListener('click', function(e) {
var minAttr = fDef.min !== undefined ? ' min="' + esc(String(fDef.min)) + '"' : '';
var maxAttr = fDef.max !== undefined ? ' max="' + esc(String(fDef.max)) + '"' : '';
td.innerHTML = '';
+ '"' + minAttr + maxAttr + ' class="form-input inline-edit-input" data-validate="positive_int"/>' +
+ '';
+ if (typeof validateEl === 'function') validateEl(td.querySelector('input'));
} else if (inputType === 'textarea') {
var textVal;
try { var arr = JSON.parse(val); textVal = Array.isArray(arr) ? arr.join('\n') : String(val||''); }
@@ -2254,21 +2271,30 @@ var validateEl;
invalid_struct: 'Invalid domain format' },
networkname: { invalid_char: 'Letters, digits, hyphens and underscores only',
invalid_struct: 'No leading, trailing or consecutive special characters' },
- time_24h: { invalid_char: 'Digits and colon only', invalid_struct: 'Must be HH:MM in 24-hour format (e.g. 02:30)' }
+ time_24h: { invalid_char: 'Digits and colon only', invalid_struct: 'Must be HH:MM in 24-hour format (e.g. 02:30)' },
+ positive_int: { invalid_char: 'Digits only',
+ out_of_range: function(el) {
+ var mn = (el && el.min !== '') ? el.min : null;
+ var mx = (el && el.max !== '') ? el.max : null;
+ if (mn !== null && mx !== null) return 'Must be between ' + mn + ' and ' + mx;
+ if (mn !== null) return 'Must be ≥ ' + mn;
+ if (mx !== null) return 'Must be ≤ ' + mx;
+ return 'Out of range';
+ }}
};
var _classifiers = { ip: classifyIp, ipv4: classifyIpv4, ipv6: classifyIpv6, mac: classifyMac,
subnet: classifySubnet, url: classifyUrl,
port: classifyPort, ipv4cidr: classifyIpv4Cidr,
endpoint: classifyEndpoint,
dashname: classifyDashname, domainname: classifyDomainname, networkname: classifyNetworkname,
- time_24h: classifyTime24h };
+ time_24h: classifyTime24h, positive_int: classifyPositiveInt };
validateEl = function(el) {
var list = el.closest('.editable-list[data-validate]');
var vtype = el.dataset.validate || (list ? list.dataset.validate : '');
var classify = _classifiers[vtype];
if (!classify) return;
- var cls = classify(el.value);
+ var cls = classify(el.value, el);
if (list) {
el.classList.remove('field-invalid', 'field-warning');
if (cls === 'incomplete') el.classList.add('field-warning');
@@ -2280,7 +2306,8 @@ var validateEl;
} else if (cls === 'incomplete') {
setFieldHint(el, el._postValidate ? el._postValidate(cls) : '', 'warning');
} else {
- setFieldHint(el, msgs[cls] || 'Invalid', 'error');
+ var msgVal = msgs[cls];
+ setFieldHint(el, typeof msgVal === 'function' ? msgVal(el) : (msgVal || 'Invalid'), 'error');
}
}
};
diff --git a/docker/routlin-dash/data/page_content.json b/docker/routlin-dash/data/page_content.json
index b839d4c..a052da1 100644
--- a/docker/routlin-dash/data/page_content.json
+++ b/docker/routlin-dash/data/page_content.json
@@ -974,7 +974,8 @@
},
{
"col": "enabled",
- "input_type": "checkbox"
+ "input_type": "checkbox",
+ "checkbox_label": "Enabled"
}
]
},
@@ -1100,7 +1101,8 @@
},
{
"col": "enabled",
- "input_type": "checkbox"
+ "input_type": "checkbox",
+ "checkbox_label": "Enabled"
}
]
},
@@ -1519,15 +1521,18 @@
},
{
"col": "radius_default",
- "input_type": "checkbox"
+ "input_type": "checkbox",
+ "checkbox_label": "Enabled"
},
{
"col": "mdns_reflection",
- "input_type": "checkbox"
+ "input_type": "checkbox",
+ "checkbox_label": "Enabled"
},
{
"col": "dnsmasq_log_queries",
- "input_type": "checkbox"
+ "input_type": "checkbox",
+ "checkbox_label": "Record"
},
{
"col": "use_blocklists",
@@ -1754,7 +1759,8 @@
},
{
"col": "enabled",
- "input_type": "checkbox"
+ "input_type": "checkbox",
+ "checkbox_label": "Enabled"
}
]
},
@@ -1920,7 +1926,8 @@
},
{
"col": "enabled",
- "input_type": "checkbox"
+ "input_type": "checkbox",
+ "checkbox_label": "Enabled"
}
]
},
@@ -2128,11 +2135,13 @@
},
{
"col": "radius_client",
- "input_type": "checkbox"
+ "input_type": "checkbox",
+ "checkbox_label": "Enabled"
},
{
"col": "enabled",
- "input_type": "checkbox"
+ "input_type": "checkbox",
+ "checkbox_label": "Enabled"
}
]
},
@@ -2326,11 +2335,13 @@
},
{
"col": "split_tunnel",
- "input_type": "checkbox"
+ "input_type": "checkbox",
+ "checkbox_label": "Enabled"
},
{
"col": "enabled",
- "input_type": "checkbox"
+ "input_type": "checkbox",
+ "checkbox_label": "Enabled"
}
]
},