PageSourceSearch

http://kiwisdr.com/kiwi/w3_util.js

js kiwisdr.com collected 2026-10-01 11:49:00 UTC 154,230 bytes, 5,104 lines download raw bytes

1// Copyright (c) 2016-2026 John Seamons, ZL4VO/KF6VO
2
3/*
4
5	////////////////////////////////
6	// API summary (w3_*)
7	////////////////////////////////
8	
9	                           integrated: L=label T=text 3=psa3()
10	                           |  callback
11	                              |  first: F=always false, not called on instan
12	                                 |
13	nav navdef
14
15	label set_label
16
17	link
18   
19	radio_button               T     no label option needed
20	radio_button_get_param     T     w3-defer
21	radio_unhighlight
22	
23	switch                     T
24	switch_label               TL    rename w3_switch_label() to w3_switch()? Any external API users?
25	switch_label_get_param           w3-defer
26	switch_set_value
27	switch_get_param                 w3-defer
28	
29	button                     T     w3-noactive w3-momentary w3-custom-events w3-disabled
30    button_path
31    button_text
32	icon                       T     w3-momentary
33	 spin
34	
35	input                      L3 cb F
36	input_change
37	input_get                        w3-defer
38	
39	textarea                   L
40	textarea_get_param         L     w3-defer
41	
42	checkbox                   L3
43	checkbox_get_param         L     w3-defer
44	checkbox_get
45	checkbox_set
46	
47	select                     L3
48	select_hier                L
49	select_get_param           L     w3-defer
50	select_enum
51	select_value
52	select_set_if_includes
53	
54	slider                     L3
55	slider_set
56	slider_get_param                 w3-defer
57	
58	menu
59	menu_items
60	menu_popup
61	menu_onclick
62	
63	inline                     3
64	inline_percent             3
65	divs                       3
66	col_percent                3
67	
68	
69	////////////////////////////////
70	// Useful stuff
71	////////////////////////////////
72	
73	<element attribute="attribute-values ..." inline-style-attribute="properties ...">
74	"styles" refers to any style-attribute="properties ..." combination
75
76	style properties set one of three ways:
77		1) in an element as above
78		
79		2) in a .css:
80		selector
81		{
82			property: property-value;
83		}
84		
85		3) assigned by DOM:
86		el.style.property = property-value;
87	
88	priority (specificity)
89	
90		[inline-style attribute], [id], [class, pseudo-class, attribute], [elements]
91		
92		The "count" at each level is determined by the number of specifiers, hence the "specific-ness"
93		e.g. ul#nav li.active a => 0,1,1,3 with 3 due to the ul, li and a
94		This would have priority over 0,1,1,2
95	
96	select (menu) behavior:
97	   If you set the background-color inline then border reverts to the undesirable beveled appearance!
98
99	in w3.css:
100		w3-show-block
101		w3-show-inline-block
102		
103		w3-section: margin TB 16px
104		w3-container: padding TB 0.01em LR 16px
105		w3-row:after: content: ""; display: table; clear: both
106		w3-col: float left, width 100%
107		w3-padding: T 8px, B 16px
108		w3-row-padding: LR 8px
109		w3-margin: TBLR 16px
110
111	in w3_ext.css:
112		w3-show-inline
113		w3-valign
114		w3-override-(colors)
115
116	id="foo" on...(e.g. onclick)=func(this.id)
117
118
119	Be sure to document our special side-effects:
120	   w3-label-inline
121
122
123	////////////////////////////////
124	// Notes about HTML/DOM
125	////////////////////////////////
126	
127	"Typically, the styles are merged, but when conflicts arise, the later declared style will generally win
128	(unless the !important attribute is specified on one of the styles, in which case that wins).
129	Also, styles applied directly to an HTML element take precedence over CSS class styles."
130	
131	window.
132		inner{Width,Height}		does not include tool/scroll bars
133		outer{Width,Height}		includes tool/scroll bars
134	
135	element.
136		client{Width,Height}		viewable only; no: border, scrollbar, margin; yes: padding
137		offset{Width,Height}		viewable only; includes padding, border, scrollbars
138
139	
140	////////////////////////////////
141	// FIXME cleanups
142	////////////////////////////////
143	
144	some routines that return nothing now could return el from internal w3_el() so
145	   caller could do chained references (see w3_innerHTML(), w3_show() ...)
146	
147	needs uniform instantiation callbacks
148	   - w3_nav
149	   x w3_navdef
150	   >>> w3_radio_button
151	   >>> w3_switch
152	   - w3_button (only 1 initial state)
153	   - w3_icon
154	   ??? w3_input(_psa): integrate, 
155	   - w3_textarea
156	   x w3_checkbox
157	   x w3_select
158	   x w3_slider
159	   - w3_menu (no initial selection)
160	
161	general conversion to w3-flex layout
162	w3_col_percent => w3_inline_percent
163	kiwi/{admin,admin_sdr}.js: w3_select('' => w3_select(w3-text-red
164	unify color setting: style.color vs w3-color, w3-text-color
165	uniform default/init control values
166	preface internal routines/vars with w3int_...
167	move some routines (esp HTML) out of kiwi_util.js into here?
168	make all 'id-', 'cl-' use uniform
169	collapse into one func the setting of cfg value and el/control current displayed value
170	w3-valign vs w3-show-inline vs w3-show-inline-block
171
172	x use DOM el.classList.f() instead of ops on el.className
173	x normalize use of embedded labels
174	
175	
176	////////////////////////////////
177	// API users
178	////////////////////////////////
179	
180	1) kiwisdr.com website content
181	
182	2) antenna switch extension is a user of API:
183	   w3_div()
184	   w3_divs()
185	   w3_inline()                   only in OLDER versions of the ant_switch ext
186	   w3_string_set_cfg_cb()
187	   w3_highlight()
188	   w3_unhighlight()
189	   w3_radio_unhighlight()
190	   var w3_highlight_time
191	   
192	   ext:
193	      ext_{get,set}_cfg_param
194	         EXT_SAVE, EXT_NO_SAVE
195	      ext_set_controls_width_height
196	      ext_send
197	      ext_admin_config
198	      ext_param
199	      ext_panel_show
200	      ext_switch_to_client
201
202*/
203
204
205var w3 = {
206   CAPTURING: true,
207   BUBBLING: false,
208   RETAIN_FOCUS: true,
209   
210   _last_: 0
211};
212
213var w3int = {
214   btn_grp_uniq: 0,
215   alert: {},
216   alert_seq: 0,
217   
218   menu_cur_id: null,
219   menu_active: false,
220   menu_debug: false,
221   menu_close_func: null,
222   prev_menu_hover_el: null,
223   
224   reload_timeout: null,
225   reload_seq: 0,
226   
227   rate_limit: {},
228   rate_limit_need_shift: {},
229   rate_limit_evt: {},
230   
231   eventListener: [],
232   
233   _last_: 0
234};
235
236
237////////////////////////////////
238// util
239////////////////////////////////
240
241var w3_console = {};
242
243// XXX: Made another attempt to get this to work correctly.
244// But is still has lots of corner-case failures compared to "console.log('foo='+ foo +' bar='+ bar)"
245// e.g. wrt undefined, null and object/function values
246w3_console.log = function(obj, prefix)
247{
248   var s = prefix? (prefix + ': ') : '';
249   if (isString(obj)) {
250      s += '"'+ obj.toString() +'"';
251   } else
252   if (isObject(obj)) {
253      console.log(obj);
254      var json = 'JSON '+ JSON.stringify(obj, w3int_console_replacer);
255      if (json.includes('\n'))
256         json = JSON.stringify(obj, w3int_console_replacer, 1);   // if json is multi-line, redo using pretty-print
257      s += json;
258   } else {
259      s += obj.toString();
260   }
261   console.log(s);
262   //if (isObject(obj)) console.log(obj);
263};
264
265function w3int_console_replacer(k, v)
266{
267   if (isUndefined(v)) return '<undefined>';
268   if (isNull(v)) return '<null>';
269   if (isFunction(v)) return '<function>';
270   return '<'+ v.toString() +'>';
271}
272
273function w3_json_to_string_no_decode(id, s)
274{
275   s = s.replace(/\\"/g, '"');
276   return s;
277}
278
279function w3_json_to_string(id, s)
280{
281   s = kiwi_decodeURIComponent(id, s);
282   return w3_json_to_string_no_decode(id, s);
283}
284
285function w3_json_to_html_no_decode(id, s)
286{
287   //console.log('1> '+ kiwi_string_to_hex(s, '-'));
288   //console.log('1> '+ JSON.stringify(s));
289   s = s.replace(/\\n/g, '<br>').replace(/\n/g, '<br>').replace(/\\"/g, '"');
290   //console.log('2> '+ JSON.stringify(s));
291   return s;
292}
293
294function w3_json_to_html(id, s)
295{
296   s = kiwi_decodeURIComponent(id, s);
297   return w3_json_to_html_no_decode(id, s);
298}
299
300function w3_html_to_nl(s)
301{
302   return s.replace(/<br>/g, '\n').replace(/\\n/g, '\n');
303}
304
305function w3_strip_quotes(s)
306{
307	if (isString(s) && (s.indexOf('\'') != -1 || s.indexOf('\"') != -1))
308		return s.replace(/'/g, '').replace(/"/g, '') + ' [quotes stripped]';
309	return s;
310}
311
312function w3_esc_dq(s)
313{
314	if (isString(s) && s.indexOf('\"') != -1)
315		return s.replace(/"/g, '&quot;');
316	return s;
317}
318
319// an arguments-in-array call that silently continues if func not found
320function w3_apply(func, args_a)
321{
322   var rv;
323
324   if (isNoArg(func)) return rv;
325   //console.log('w3_call: '+ kiwi_JSON(args_a));
326   
327	try {
328	   if (isString(func)) {
329         var f = getVarFromString(func);
330         //console.log('w3_call: '+ func +'('+ kiwi_JSON(args_a) +')');
331         if (isFunction(f)) {
332            //var args = Array.prototype.slice.call(arguments);
333            rv = f.apply(null, args_a);
334         } else {
335            //console.error('w3_call: getVarFromString(func) not a function: '+ func +'('+ kiwi_JSON(args_a) +')');
336         }
337      } else
338	   if (isFunction(func)) {
339         rv = func.apply(null, args_a);
340	   } else {
341	      console.log('w3_call: func not a string or function');
342	      console.log(kiwi_JSON(func));
343	      //kiwi_trace();
344	   }
345	} catch(ex) {
346		console.log('w3_call: while in func this exception occured:');
347		console.log(ex);
348		//console.log('w3_call: '+ ex.toString());
349		//console.log(ex.stack);
350	}
351	
352	return rv;
353}
354
355// a multi-argument call that silently continues if func not found
356function w3_call(func)
357{
358   var rv;
359
360   if (isNoArg(func)) return rv;
361   var args_a = Array.from(arguments);    // works because arguments is iterable
362   args_a.shift();   // remove func
363   //console.log('w3_call: '+ kiwi_JSON(args_a));
364   rv = w3_apply(func, args_a);
365   return rv;
366}
367
368function w3_first_value(v)
369{
370   //console.log('w3_first_value');
371   //console.log(v);
372   if (v == null)
373      return 'null';
374
375   var rv = null;
376   var to_v = typeof(v);
377   
378   if (to_v === 'number' || to_v === 'boolean' || to_v === 'string') {
379      //console.log('w3_first_value prim');
380      rv = v;
381   } else
382   if (isArray(v)) {
383      //console.log('w3_first_value array');
384      rv = v[0];
385   } else
386   if (to_v === 'object') {   // first value in object no matter what the key
387      //console.log('w3_first_value obj');
388      rv = w3_obj_seq_el(v, 0);
389   } else {
390      //console.log('w3_first_value other');
391      rv = to_v;
392   }
393   //console.log('w3_first_value rv='+ rv);
394   return rv;
395}
396
397function w3_second_value(v)
398{
399   //console.log('w3_second_value');
400   //console.log(v);
401   var rv = null;
402   
403   if (isArray(v) && v.length > 1) {
404      //console.log('w3_second_value array[1]');
405      if (v.length > 1)
406         rv = v[1];
407   }
408
409   //console.log('w3_second_value rv='+ rv);
410   return rv;
411}
412
413function w3_opt(opt, elem, default_val, pre, post)
414{
415   //console.log('w3_opt opt:');
416   //console.log(opt);
417   var s;
418   if (isArg(opt) && isDefined(opt[elem])) {
419      //console.log('w3_opt elem='+ elem +' DEFINED rv='+ opt[elem]);
420      s = opt[elem];
421   } else {
422      //console.log('w3_opt elem='+ elem +' NOT DEFINED rv='+ default_val);
423      s = default_val;
424   }
425   if (isNonEmptyString(s)) {
426      if (isNonEmptyString(pre)) s = pre + s;
427      if (isNonEmptyString(post)) s = s + post;
428   }
429   return s;
430}
431
432function w3_obj_num(o)
433{
434   if (isUndefined(o) || isNull(o) || isObject(o)) return o;
435   if (isNumber(o)) return { num: o };
436   if (isBoolean(o)) return { num: (o? 1:0) };
437   if (isString(o)) return { num: parseFloat(o) };
438   return o;
439}
440
441function w3_obj_key_count(obj) { return Object.keys(obj).length; }
442
443// given a sequential position in an object's keys, return the corresponding object element for that key
444function w3_obj_seq_el(obj, idx)
445{
446   var keys_a = Object.keys(obj);
447   if (!isNumber(idx) || idx < 0 || idx >= keys_a.length) return null;
448   return obj[keys_a[idx]];
449}
450
451// given an object key, return its sequential position in that object
452function w3_obj_key_seq(obj, key)
453{
454   var seq = null;
455   Object.keys(obj).forEach(function(_key, i) {
456      if (_key == key) seq = i;
457   });
458   return seq;
459}
460
461function w3_array_el_seq(arr, el, opt)
462{
463   var toLower = w3_opt(opt, 'toLower', false);
464   var seq = null;
465   arr.forEach(function(a_el, i) {
466      var ael = toLower? a_el.toLowerCase() : a_el;
467      if (ael == el) seq = i;
468   });
469   return seq;
470}
471
472function w3_obj_enum(obj, func, opt)
473{
474   var key_match = w3_opt(opt, 'key_match', undefined);
475   var value_match = w3_opt(opt, 'value_match', undefined);
476
477   Object.keys(obj).forEach(function(key, i) {
478      var o = obj[key];
479      if (isDefined(key_match)) {
480         if (key == key_match) func(key, i, o);
481      } else
482      if (isDefined(value_match)) {
483         if (o == value_match) func(key, i, o);
484      } else {
485         func(key, i, o);
486      }
487   });
488}
489
490function w3_enum_obj_or_array_of_objs(obj_arr, func)
491{
492   if (isArray(obj_arr)) {
493      obj_arr.forEach(function(obj) {
494         func(obj);
495      });
496   } else
497   
498   if (isObject(obj_arr)) {
499      func(obj_arr);
500   }
501}
502
503// merge the object fields of osrc into odst
504function w3_obj_mix(odst, osrc)
505{
506   if (isArg(osrc))
507      w3_obj_enum(osrc, function(key, i, o) {
508         odst[key] = o;
509      });
510   return odst;
511}
512
513// arr:     [] string vals to iterate over
514// s:       string val to match with startsWith() (case-insensitive)
515// func:    (optional) called as func(arr-idx, val) only on first match
516// returns: found = true|false
517function w3_ext_param_array_match_str(arr, s, func)
518{
519   var found = false;
520   arr.forEach(function(a_el, i) {
521      var el = a_el.toString().toLowerCase();
522      //console.log('w3_ext_param_array_match_str CONSIDER '+ i +' '+ el +' '+ s);
523      if (!found && el.startsWith(s)) {
524         //console.log('w3_ext_param_array_match_str MATCH '+ i);
525         if (func) func(i, a_el.toString());
526         found = true;
527      }
528   });
529   return found;
530}
531
532// arr:     [] vals to iterate over
533// n:       num val to match
534// func:    (optional) called as func(arr-idx, val) only on first match
535// returns: found = true|false
536function w3_ext_param_array_match_num(arr, n, func)
537{
538   var found = false;
539   arr.forEach(function(a_el, i) {
540      var a_num = parseFloat(a_el);
541      //console.log('w3_ext_param_array_match_num CONSIDER '+ i +' '+ a_num +' '+ n);
542      if (!found && a_num == n) {
543         //console.log('w3_ext_param_array_match_num MATCH '+ i);
544         if (func) func(i, n);
545         found = true;
546      }
547   });
548   return found;
549}
550
551// s:       name.startsWith(s)   case-insensitive
552//          null                 wildcard
553// param:   name[:val]
554// returns: { match:true|false, full_match:true|false, has_value:true|false, num:parseFloat(val), string:val, string_case:val, items:[] }
555function w3_ext_param(s, param)
556{
557   var rv = { match:false, full_match:false, has_value:false };
558   var pu = param.split(':');
559   var pl = param.toLowerCase().split(':');
560   //console.log('w3_ext_param: s='+ s +' pu='+ pu[0] +'|'+ pu[1]);
561   if (isNull(s) || s.startsWith(pl[0])) {
562      rv.match = true;
563      if (isNull(s)) {
564         rv.has_value = true;
565         rv.num = parseFloat(param);
566         rv.string = param;
567         rv.string_case = param;
568         rv.items = [];
569      } else
570      if (pl.length > 1) {
571         rv.has_value = true;
572         rv.num = parseFloat(pl[1]);
573         
574         // allow string value to contain ':'
575         rv.string = pl.slice(1).join(':');
576         rv.string_case = pu.slice(1).join(':');
577         rv.items = pu;
578      } else {
579         rv.num = 0;
580         rv.string = rv.string_case = '';
581         rv.items = [];
582      }
583   }
584   if (s && s == pl[0]) rv.full_match = true;
585   //console.log(rv);
586   return rv;
587}
588
589function w3_clamp(v, min, max, clamp_val)
590{
591   if (isNaN(v)) return (isDefined(clamp_val)? clamp_val : NaN);
592   
593   if (isObject(min)) {
594      var o = min;
595      try {
596         if (typeof(o.clamp) === 'undefined') {
597            v = Math.max(o.min, Math.min(o.max, v));
598         } else {
599            if (v < o.min || v > o.max) v = o.clamp;
600         }
601      } catch(ex) {
602         console.log('w3_clamp: bad obj:');
603         console.log(o);
604         return undefined;
605      }
606   } else {
607      if (clamp_val == undefined)
608         v = Math.max(min, Math.min(max, v));
609      else
610         if (v < min || v > max) v = clamp_val;
611   }
612   
613   return v;
614}
615
616// to conditionally test for out-of-range use as:
617// if (w3_clamp3(v, min, max)) { in-range } else { out-of-range }
618function w3_clamp3(v, min, max, clamp_min, clamp_max, clamp_NaN)
619{
620   if (isUndefined(clamp_min)) clamp_min = NaN;
621   if (isUndefined(clamp_max)) clamp_max = NaN;
622   if (isUndefined(clamp_NaN)) clamp_NaN = NaN;
623   if (isNaN(v)) return clamp_NaN;
624   if (v < min) return clamp_min;
625   if (v > max) return clamp_max;
626   return v;
627}
628
629function w3_wrap(v, min, max)
630{
631   if (v < min) v = max; else
632   if (v > max) v = min;
633   return v;
634}
635
636// This works, but clipboard results in double newlines (\n\n) if e.g. multiple <pre> elements inside a <code>
637// But it is useful to visually show select-all selection.
638/*
639function w3_select_all_visual(id) {
640    var div = w3_el(id);
641    if (!div) return;
642
643    // create a range that covers the entire div
644    var range = document.createRange();
645    range.selectNodeContents(div);
646    
647    var selection = window.getSelection();
648    selection.removeAllRanges();    // clear any existing selection
649    selection.addRange(range);
650}
651*/
652
653function w3_copy_to_clipboard_el(id, func) {
654    var el = w3_el(id);
655    if (!el) return;
656    var s = '';
657    w3_iterate_children(el,      // only the nodes one level deep
658      function(child, i) {
659         //console.log(child);
660         //console.log('i='+ i +' <'+ child.textContent +'>');
661         var t = child.textContent;
662         if (isFunction(func)) t = func(t);
663         s += t +'\n';
664      }
665   );
666   //console.log('s='+ s);
667   
668   w3_copy_to_clipboard(s);
669   //w3_select_all_visual(el);
670   w3_alert('w3_copy_to_clipboard_alert', '', 'copied to clipboard', null, {width:'180px', fadeout:1000});
671}
672
673function w3_copy_to_clipboard_deep_el(id, func) {
674    var el = w3_el(id);
675    if (!el) return;
676    var s = '';
677    w3_iterateDeep_children(el,
678      function(child) {
679         s += func(child);
680      }
681   );
682   s += '\n';
683   //console.log('s='+ s);
684   
685   w3_copy_to_clipboard(s);
686   //w3_select_all_visual(el);
687   w3_alert('w3_copy_to_clipboard_deep_alert', '', 'copied to clipboard', null, {width:'180px', fadeout:1000});
688}
689
690function w3_copy_to_clipboard(s)
691{
692   // this isn't available on non-https connections
693   //navigator.clipboard.writeText(s.trim());
694   
695   // workaround for http connections using execCommand('copy')
696   // NB: This tiny, invisible, readonly textarea element prevents Firefox from doing an inward autozoom.
697   // Also, it is necessary to use textarea and not an input element to preserve newlines.
698   var el = document.createElement("textarea");
699   el.value = s;
700   el.style.position = "fixed";
701   el.style.left = "-99999px";
702   el.style.top = "-99999px";
703   el.style.width = "1px";
704   el.style.height = "1px";
705   el.style.padding = "0";
706   el.style.border = "0";
707   el.style.opacity = "0";
708   el.style.zIndex = "-9999";
709   el.setAttribute("readonly", "readonly");
710   document.body.appendChild(el);
711   el.focus();
712   el.select();
713   document.execCommand('copy');
714   document.body.removeChild(el);
715}
716
717////////////////////////////////
718// HTML
719////////////////////////////////
720
721function w3_add_id(path, suffix)
722{
723   if (!isString(path)) return path;
724   path = path || '';
725   if (path == '' || path == 'body') return path;
726   path = path.startsWith('id-')? path : ('id-'+ path);
727   suffix = suffix || '';
728   return path + suffix;
729}
730
731// return document element reference either by id or name
732function w3int_w3_el(id_name_class)
733{
734   if (!id_name_class || id_name_class == '') return null;
735	var el = document.getElementById(id_name_class);
736	if (el == null) {
737		el = document.getElementsByName(id_name_class);		// 'name=' is deprecated
738		if (el != null) el = el[0];	// use first from array
739	}
740	if (el == null) {
741		el = document.getElementsByClassName(id_name_class);
742		if (el != null) el = el[0];	// use first from array
743	}
744	return el;
745}
746
747function w3int_w3_els(id_name_class)
748{
749   if (!id_name_class || id_name_class == '') return null;
750	var els = document.getElementById(id_name_class);
751	var rv;
752	if (els != null) {
753	   rv = [els];
754	} else {
755      els = document.getElementsByName(id_name_class);		// 'name=' is deprecated
756      if (els == null || els.length == 0) {
757         els = document.getElementsByClassName(id_name_class);
758      }
759      rv = els;
760   }
761   if (rv.length == 0) rv = null;
762	return rv;  // returns single-element array or iterable HTMLCollection
763}
764
765// allow an element-obj or element-id to be used
766// try id without, then with, leading 'id-'; then including cfg prefix as a last resort
767function w3_el(el_id)
768{
769	if (isString(el_id)) {
770	   if (el_id == '') return null;
771	   if (el_id == 'body') return document.body;
772		var el = w3int_w3_el(el_id);
773		if (el == null) {
774			el = w3int_w3_el(w3_add_id(el_id));
775			if (el == null) {
776				el_id = w3_add_toplevel(el_id);
777				el = w3int_w3_el(el_id);
778				if (el == null) {
779					el = w3int_w3_el(w3_add_id(el_id));
780				}
781			}
782		}
783		return el;
784	}
785	return (el_id);
786}
787
788// for when there are multiple elements with the same "id-*"
789// returns element list and optionally calls iterates calling func()
790function w3_els(el_id, func)
791{
792	if (isString(el_id)) {
793	   if (el_id == '') return null;
794	   if (el_id == 'body') return document.body;
795		var els = w3int_w3_els(el_id);
796		if (els == null) {
797			els = w3int_w3_els(w3_add_id(el_id));
798			if (els == null) {
799				el_id = w3_add_toplevel(el_id);
800				els = w3int_w3_els(el_id);
801				if (els == null) {
802					els = w3int_w3_els(w3_add_id(el_id));
803				}
804			}
805		}
806      if (els && func) for (var i = 0; i < els.length; i++) {
807         func(els[i], i);
808      }
809		return els;
810	}
811	
812	var el = w3_el(el_id);
813	if (func) {
814	   func(el, 0);
815	}
816	return [el];
817}
818
819// return id of element (i.e. 'id-*') if found
820function w3_id(el_id)
821{
822	var el = w3_el(el_id);
823	if (!el) return 'id-NULL';
824	if (el.id && el.id.startsWith('id-')) return el.id;
825	var done = false;
826	var id = null;
827	w3_iterate_classList(el, function(className, idx) {
828      //console.log('w3_id CONSIDER '+ className);
829	   if (className.startsWith('id-') && !done) {
830	      id = className;
831	      done = true;
832	   }
833	});
834	return (id? id : 'id-NULL');
835}
836
837function w3_activeElement()
838{
839   // returns 'id-NULL' if activeElement is null
840   return w3_id(document.activeElement);
841}
842
843function w3_noElHasFocus()
844{
845   var ae = w3_activeElement();
846   //console.log('ae='+ ae);
847   return (ae == 'id-kiwi-body');
848}
849
850// assign innerHTML, silently failing if element doesn't exist
851function w3_innerHTML(id)
852{
853	//var el = w3_el_softfail(id);
854	var el = w3_el(id);
855	if (!el) return null;
856	
857	// get
858	if (arguments.length == 1)
859	   return el.innerHTML;
860	
861	// set
862	var s = '';
863	var narg = arguments.length;
864   for (var i=1; i < narg; i++) {
865      s += arguments[i];
866   }
867	el.innerHTML = s;
868	return el;
869}
870
871function w3_clearInnerHTML(id)
872{
873   w3_innerHTML(id, '');
874}
875
876function w3_append_innerHTML(id)
877{
878	var el = w3_el(id);
879	if (!el) return null;
880	var s = '';
881	var narg = arguments.length;
882   for (var i=1; i < narg; i++) {
883      s += arguments[i];
884   }
885	el.innerHTML += s;
886	return el;
887}
888
889function w3_get_innerHTML(id)
890{
891	var el = w3_el(id);
892	if (!el) return null;
893	return el.innerHTML;
894}
895
896function w3_iterate_classname(cname, func)
897{
898	var els = document.getElementsByClassName(w3_add_id(cname));
899	if (els == null) return;
900	for (var i=0; i < els.length; i++) {      // els is a collection, can't use forEach()
901		func(els[i], i);
902	}
903}
904
905function w3_iterate_classList(el_id, func)
906{
907	var el = w3_el(el_id);
908	if (el && el.classList) for (var i = 0; i < el.classList.length; i++) {     // el.classList is a collection, can't use forEach()
909		func(el.classList.item(i), i);
910	}
911	return el;
912}
913
914function w3_create_appendElement(el_parent, el_type, html, id, psa)
915{
916   var el_child = document.createElement(el_type);
917   if (isNonEmptyString(id)) el_child.id = id;
918   if (isNonEmptyString(psa)) w3_set_psa(el_child, psa);
919   if (isNonEmptyString(html)) w3_innerHTML(el_child, html);
920	w3_el(el_parent).appendChild(el_child);
921	return el_child;
922}
923
924/*
925not currently used
926// NB: only elements, not text and comments nodes
927function w3_trim_childElements(el_parent, max_len)
928{
929	var el = w3_el(el_parent);
930	while (el.children.length > max_len)
931	   el.removeChild(el.firstElementChild);
932}
933*/
934
935function w3_iterate_parent(el_id, func)
936{
937	var el = w3_el(el_id);
938	var i = 0;
939	
940	do {
941		if (func(el, i) != null)
942		   break;
943		el = el.parentNode;
944		i++;
945	} while (el);
946}
947
948// excludes text and comment nodes
949function w3_iterate_children(el_id, func)
950{
951	var el = w3_el(el_id);
952	if (!el) return null;
953	
954	for (var i=0; i < el.children.length; i++) {    // el.children is a collection, can't use forEach()
955		var child_el = el.children[i];
956		func(child_el, i);
957	}
958}
959
960// excludes text and comment nodes
961function w3_iterateDeep_children(el_id, func, level)
962{
963	var el = w3_el(el_id);
964	if (!el) return null;
965	if (isUndefined(level)) level = 0;
966	
967	for (var i=0; i < el.children.length; i++) {    // el.children is a collection, can't use forEach()
968		var child_el = el.children[i];
969		func(child_el, level, i);
970		if (child_el.hasChildNodes)
971			w3_iterateDeep_children(child_el, func, level+1);
972	}
973}
974
975// includes text and comment nodes
976function w3_iterate_childNodes(el_id, func)
977{
978	var el = w3_el(el_id);
979	
980	for (var i=0; i < el.childNodes.length; i++) {    // el.childNodes is a collection, can't use forEach()
981		var child_el = el.childNodes[i];
982		func(child_el, i);
983	}
984}
985
986function w3_width_height(el_id, w, h)
987{
988	var el = w3_el(el_id);
989	if (!el) return null;
990	
991	if (isArg(w)) {
992	   if (isNumber(w)) w = px(w);
993	   el.style.width = w;
994	}
995
996	if (isArg(h)) {
997	   if (isNumber(h)) h = px(h);
998	   el.style.height = h;
999	}
1000	
1001	w = el.style.width;
1002	h = el.style.height;
1003	return { w:parseInt(w), width:w, h:parseInt(h), height:h };
1004}
1005
1006function w3_border(el_id, s)
1007{
1008	var el = w3_el(el_id);
1009	if (!el) return null;
1010	el.style.border = s;
1011	return el;
1012}
1013
1014// bounding box measured from the origin of parent
1015function w3_boundingBox_children(el_id, debug)
1016{
1017   var el = w3_el(el_id);
1018	var bbox = { id:el_id, x1:1e99, x2:0, y1:1e99, y2:0, w:0, h:0 };
1019	var found = false;
1020	w3_iterateDeep_children(el_id, function(el, level, i) {
1021		if (el.nodeName != 'DIV' && el.nodeName != 'SPAN' && el.nodeName != 'IMG')
1022			return;
1023		var position = css_style(el, 'position');
1024		if (position == 'static')
1025		   return;
1026		if (el.offsetHeight == 0)
1027		   return;
1028		if (debug) console.log('level='+ level +' i='+ i);
1029		if (debug) console.log(el);
1030		if (!found) { bbox.el = el; found = true; }
1031		//console.log(el.offsetParent);
1032		if (debug) console.log(el.nodeName +' el.oL='+ el.offsetLeft +' el.oW='+ el.offsetWidth +' el.oT='+ el.offsetTop +' el.oH='+ el.offsetHeight +' '+ position);
1033
1034		bbox.x1 = Math.min(bbox.x1, el.offsetLeft);
1035		var x2 = el.offsetLeft + el.offsetWidth;
1036		bbox.x2 = Math.max(bbox.x2, x2);
1037
1038		bbox.y1 = Math.min(bbox.y1, el.offsetTop);
1039		var y2 = el.offsetTop + el.offsetHeight;
1040		bbox.y2 = Math.max(bbox.y2, y2);
1041	});
1042
1043	bbox.w = bbox.x2 - bbox.x1;
1044	bbox.h = bbox.y2 - bbox.y1;
1045	if (bbox.w == -1e+99) { bbox.w = bbox.h = 0; }
1046	//if (debug) w3_console.log(bbox, 'w3_boundingBox_children');
1047	if (debug) console.log(bbox);
1048	return bbox;
1049}
1050
1051function w3_center_in_window(el_id, id)
1052{
1053	var el = w3_el(el_id);
1054	var rv = (window.innerHeight - el.clientHeight) / 2;
1055	//console.log('w3_center_in_window wh='+ window.innerHeight +' ch='+ el.clientHeight +' rv='+ rv + (id? (' '+ id) : ''));
1056	return rv;
1057}
1058
1059// conditionally select field element
1060// also used to dismiss popup keyboard on mobile via el.blur()
1061function w3_field_select(el_id, opts)
1062{
1063	var el = w3_el(el_id);
1064	el = (el && isFunction(el.select))? el : null;
1065
1066   opts = opts || {};
1067   var trace = opts.trace;
1068   if (trace) {
1069      var id = isObject(el_id)? el_id.id : el_id;
1070      console.log('w3_field_select id='+ id +' el='+ el +' v='+ (el? el.value:null));
1071      console.log(el);
1072      console.log(opts);
1073   }
1074   if (!el) return;
1075   
1076   // If ext_set_scanning() has set extint.scanning then don't perform a field select
1077   // if this element specifies the "scanning:1" option.
1078   //
1079   // The ALE_2G extension currently does this so its control panel UI items can be used
1080   // without being interrupted by the frequency entry field taking focus away during
1081   // each scan frequency tune.
1082   if (opts.scanning && extint.scanning) return;
1083   
1084   var focus=0, select=0, blur=0, dismiss=0;
1085   if (opts.mobile) {
1086      if (kiwi_isMobile()) blur = 1; else focus = select = 1;
1087   }
1088   if (opts.blur) blur = 1;
1089   if (opts.dismiss_kybd && kiwi_isMobile()) dismiss = 1;
1090   if (opts.focus_select) focus = select = 1;
1091   if (opts.focus_only) { focus = 1; select = 0; }
1092   if (opts.select_only) select = 1;
1093   
1094   if (trace) console.log('w3_field_select focus='+ focus +' select='+ select +' blur='+ blur +' dismiss='+ dismiss);
1095   //if (opts.log) canvas_log('$'+ opts.log +': F='+ focus +' S='+ select +' B='+ blur +' D='+ dismiss +' S='+ TF(kiwi_isOld_iOS()));
1096   
1097   // old iPad-2 safari/iOS does this weird repeating keyboard popup if keyboard blurred this way
1098   if (kiwi_isOld_iOS()) dismiss = 0;
1099
1100   if (focus) el.focus();
1101   if (select) el.select();
1102   if (blur) el.blur();
1103   if (dismiss) el.blur();
1104   if (trace) kiwi_trace();
1105}
1106
1107function w3_dismiss_keyboard(el)
1108{
1109   w3_field_select(el, {dismiss_kybd:1});
1110}
1111
1112// add, remove or check presence of class properties
1113function w3_add(el_id, props)
1114{
1115   if (!el_id || isEmptyString(props)) return null;
1116   
1117   var first_el = null;
1118   // FIXME: why doesn't this work?!?
1119   //w3_els(el_id, function(el) {
1120   var el = w3_el(el_id);
1121      if (!el) return null;
1122      if (first_el) return;
1123      //console.log('$w3_add '+ el_id +' <'+ props +'>');
1124      if (!el || !props) return null;
1125      props = props.replace(/\s+/g, ' ').split(' ');
1126      props.forEach(function(p) {
1127         el.classList.add(p);
1128      });
1129      if (!first_el) first_el = el;
1130   //});
1131
1132	return first_el;
1133	//return el;
1134}
1135
1136// remember selector syntax: #id .class
1137function w3_add_by_selector(selector, props)
1138{
1139   var node_list = document.querySelectorAll(selector);
1140   if (isNull(node_list)) return;
1141   node_list.forEach(
1142      function(el,i) {
1143         //console.log(i +' '+ el.id);
1144         w3_add(el, props);
1145      }
1146   );
1147}
1148
1149function w3_remove(el_id, props)
1150{
1151   if (!el_id || isEmptyString(props)) return null;
1152   
1153   var first_el = null;
1154   //w3_els(el_id, function(el) {
1155   var el = w3_el(el_id);
1156      if (!el) return null;
1157      if (!first_el) first_el = el;
1158      //console.log('$w3_remove <'+ props +'>');
1159      if (!el || !props) return null;
1160      props = props.replace(/\s+/g, ' ').split(' ');
1161      props.forEach(function(p) {
1162         el.classList.remove(p);
1163      });
1164   //});
1165
1166	return first_el;
1167}
1168
1169// remember selector syntax: #id .class
1170function w3_remove_by_selector(selector, props)
1171{
1172   var node_list = document.querySelectorAll(selector);
1173   if (isNull(node_list)) return;
1174   node_list.forEach(
1175      function(el,i) {
1176         //console.log(i +' '+ el.id);
1177         w3_remove(el, props);
1178      }
1179   );
1180}
1181
1182function w3_match_wildcard(el_id, prefix)
1183{
1184	var el = w3_el(el_id);
1185	//console.log('w3_match_wildcard <'+ prefix +'>');
1186	if (!el) return null;
1187	for (var i = 0; i < el.classList.length; i++) {    // el.classList is a collection, can't use forEach()
1188	   var prop = el.classList.item(i);
1189	   //console.log('w3_match_wildcard CONSIDER <'+ prop +'>');
1190	   if (prop.startsWith(prefix)) {
1191	      var a = prop.split(prefix);
1192	      //console.log(a);
1193	      var suffix = (a.length >= 2)? a[1] : '';
1194	      return { prop:prop, suffix:suffix, num:suffix.filterInt() };
1195	   }
1196	}
1197	return false;
1198}
1199
1200function w3_remove_wildcard(el_id, prefix)
1201{
1202	var el = w3_el(el_id);
1203	//console.log('w3_remove_wildcard <'+ prefix +'>');
1204	if (!el) return null;
1205	for (var i = 0; i < el.classList.length; i++) {    // el.classList is a collection, can't use forEach()
1206	   var prop = el.classList.item(i);
1207	   if (prop.startsWith(prefix)) el.classList.remove(prop);
1208	}
1209	return el;
1210}
1211
1212function w3_set_props(el_id, props, cond)
1213{
1214	var el = w3_el(el_id);
1215	if (!el) return el;
1216	cond = isArg(cond)? cond : true;
1217
1218	if (cond)
1219	   w3_add(el, props);
1220	else
1221	   w3_remove(el, props);
1222	return el;
1223}
1224
1225function w3_remove_then_add(el_id, r_props, a_props)
1226{
1227	w3_remove(el_id, r_props);
1228	w3_add(el_id, a_props);
1229}
1230
1231function w3_remove_then_add_cond(el_id, cond, t_props, f_props)
1232{
1233	w3_remove(el_id, t_props +' '+ f_props);
1234	w3_add(el_id, cond? t_props : f_props);
1235}
1236
1237// remember selector syntax: #id .class
1238function w3_remove_then_add_cond_by_selector(selector, cond, t_props, f_props)
1239{
1240   var node_list = document.querySelectorAll(selector);
1241   if (isNull(node_list)) return;
1242   node_list.forEach(
1243      function(el,i) {
1244         //console.log(i +' '+ el.id);
1245         w3_remove_then_add_cond(el, cond, t_props, f_props);
1246      }
1247   );
1248}
1249
1250// remember selector syntax: #id .class
1251function w3_nodes_by_selector(selector)
1252{
1253   return document.querySelectorAll(selector);
1254}
1255
1256// remember selector syntax: #id .class
1257function w3_func_by_selector(selector, func)
1258{
1259   var args = Array.from(arguments);   // works because arguments is iterable
1260   args.shift(); args.shift();         // remove selector, func
1261   console.log(args);
1262   var node_list = document.querySelectorAll(selector);
1263   console.log(node_list);
1264   if (isNull(node_list)) return null;
1265   node_list.forEach(
1266      function(el,i) {
1267         console.log(i +' id='+ el.id);
1268         var a = args;
1269         a.unshift(el);    // add el
1270         console.log(a);
1271         w3_apply(func, a);
1272      }
1273   );
1274}
1275
1276function w3_contains(el_id, prop)
1277{
1278	var el = w3_el(el_id);
1279	if (!el) return 0;
1280	var clist = el.classList;
1281	return (!clist || !clist.contains(prop))? 0:1;
1282}
1283
1284function w3_parent_with(el_id, prop)
1285{
1286	var el = w3_el(el_id);
1287   if (!el) return;
1288	
1289	var found = null;
1290	w3_iterate_parent(el, function(parent) {
1291	   if (!found && w3_contains(parent, prop)) {
1292	      found = parent;
1293	   }
1294	});
1295	return found;
1296}
1297
1298function w3_toggle(el_id, prop)
1299{
1300	var el = w3_el(el_id);
1301	if (!el) return;
1302	if (w3_contains(el, prop)) {
1303		w3_remove(el, prop);
1304		//console.log('w3_toggle: hiding '+ el_id);
1305	} else {
1306		w3_add(el, prop);
1307		//console.log('w3_toggle: showing '+ el_id);
1308	}
1309	return el;
1310}
1311
1312function w3_appendAllClass(cname, prop)
1313{
1314	w3_iterate_classname(cname, function(el) { el.classList.add(prop); });
1315}
1316	
1317function w3_setAllHref(cname, href)
1318{
1319	w3_iterate_classname(cname, function(el) { el.href = href; });
1320}
1321
1322// Can't simply do "el.style.display =" for these since things like
1323// w3-hide are declared "!important" by w3.css
1324// Must actually remove/insert them from the class property.
1325function w3_show(el_id, display)
1326{
1327	var el = w3_el(el_id);
1328	if (!el) return el;
1329	
1330	w3_remove(el, 'w3-hide');
1331	w3_add(el, display? display : 'w3-show-inline-block');
1332	return el;
1333}
1334
1335function w3_create_attribute(el_id, name, val)
1336{
1337	var el = w3_el(el_id);
1338	if (el == null) return;
1339	var attr = document.createAttribute(name);
1340	attr.value = val;
1341	el.setAttributeNode(attr);
1342}
1343
1344function w3_attribute(el_id, name, val, cond)
1345{
1346	var el = w3_el(el_id);
1347	if (el == null) return null;
1348	if (isUndefined(cond) || cond == true)
1349	   el.setAttribute(name, val);   // repeated sets only update (i.e. don't create duplicate attrs)
1350	else
1351	   el.removeAttribute(name);
1352}
1353
1354// e.g. 'title="..." onclick="..."'
1355function w3_set_attributes_from_string(element, s) {
1356    var attrs = s.match(/(\S+)=["']([^"']*)["']/g) || [];
1357    
1358    attrs.forEach(
1359      function (attr) {
1360        var [key, value] = attr.split('=');
1361        var cleanKey = key.trim();
1362        var cleanValue = value.replace(/["']/g, '').trim();
1363        element.setAttribute(cleanKey, cleanValue);
1364    });
1365}
1366
1367function w3_show_block(el_id)
1368{
1369   return w3_show(el_id, 'w3-show-block');
1370}
1371
1372function w3_show_inline_block(el_id)
1373{
1374   return w3_show(el_id, 'w3-show-inline-block');
1375}
1376
1377function w3_show_inline(el_id)
1378{
1379   return w3_show(el_id, 'w3-inline-flex');
1380}
1381
1382function w3_show_table_cell(el_id)
1383{
1384   return w3_show(el_id, 'w3-show-table-cell');
1385}
1386
1387function w3_hide(el_id)
1388{
1389	var el = w3_el(el_id);
1390	if (!el) return el;
1391	
1392   //w3_console.log(el, 'w3_hide BEGIN');
1393	el = w3_iterate_classList(el, function(className, idx) {
1394      //console.log('w3_hide CONSIDER '+ className);
1395	   if (className.startsWith('w3-show-')) {
1396         //console.log('w3_hide REMOVE '+ className);
1397	      w3_remove(el, className);
1398	   }
1399	});
1400	w3_add(el, 'w3-hide');
1401   //w3_console.log(el, 'w3_hide END');
1402	return el;
1403}
1404
1405// if overrides are done correctly shouldn't need to remove any w3-show-*
1406// only toggle presence of w3-hide
1407function w3_hide2(el_id, cond)
1408{
1409	var el = w3_el(el_id);
1410	if (!el) return el;
1411	cond = isArg(cond)? cond : true;
1412	
1413	w3_set_props(el, 'w3-hide', cond);
1414	return el;
1415}
1416
1417function w3_show_hide(el_id, show, display, n_parents_up, props, cond)
1418{
1419   var rv;
1420   var el = w3_el(el_id);
1421   //w3_console.log(el, 'w3_show_hide BEGIN show='+ show);
1422   
1423   if (isNumber(n_parents_up))
1424      for (var i = 0; i < n_parents_up; i++) el = el.parentElement;
1425   
1426   if (show) {
1427      rv = w3_show(el, display? display : 'w3-show-block');
1428   } else {
1429      rv = w3_hide(el);
1430   }
1431   if (isString(props)) w3_set_props(el, props, cond);
1432   //w3_console.log(el, 'w3_show_hide END');
1433   return rv;
1434}
1435
1436function w3_show_hide_inline(el, show, n_parents_up, props, cond)
1437{
1438   w3_show_hide(el, show, 'w3-inline-flex', n_parents_up, props, cond);
1439}
1440
1441function w3_disable(el_id, disable, prop)
1442{
1443   if (!isArg(disable)) disable = true;
1444   
1445   el = w3_el(el_id);
1446   if (!el) return null;
1447   //console.log('w3_disable disable='+ disable +' t/o(el)='+ typeof(el) +' nodeName='+ el.nodeName +' type='+ el.type);
1448   //console.log(el);
1449   var input = (el.nodeName == 'INPUT');
1450   var slider = (input && el.type == 'range');
1451   var field = (input && el.type == 'text');
1452   if (!isString(prop)) prop = slider? 'w3-disabled' : 'w3-disabled-field';
1453   w3_set_props(el, prop, disable);
1454	
1455	// for disabling menu popup and sliders
1456	var attr = field? 'readonly' : 'disabled';
1457	if (isDefined(el.nodeName) && (el.nodeName == 'SELECT' || input)) {
1458	   try {
1459         if (disable)
1460            el.setAttribute(attr, '');
1461         else
1462            el.removeAttribute(attr);
1463      } catch(ex) {
1464         console.log('w3_disable:Attribute');
1465         console.log(ex);
1466      }
1467   }
1468	
1469	return el;
1470}
1471
1472function w3_disable_multi(el_id, disable, prop)
1473{
1474   w3_els(el_id,
1475      function(el, i) {
1476         w3_disable(el, disable, prop);
1477      }
1478   );
1479}
1480
1481function w3_visible(el_id, visible)
1482{
1483   if (!isArg(visible)) visible = true;
1484   
1485	var el = w3_el(el_id);
1486   if (!el) return null;
1487	el.style.visibility = visible? 'visible' : 'hidden';
1488	return el;
1489}
1490
1491function w3_isVisible(el_id)
1492{
1493	var el = w3_el(el_id);
1494	if (!el) return null;
1495	return (el.style.visibility == 'visible');
1496}
1497
1498function w3_zIndex(el_id, znew)
1499{
1500	var el = w3_el(el_id);
1501	if (!el) return null;
1502	var zold = el.style.zIndex;
1503	if (isArg(znew)) el.style.zIndex = znew;
1504	return zold;
1505}
1506
1507// our standard for confirming (highlighting) a control action (e.g.button push)
1508var w3_highlight_time = 250;
1509var w3_highlight_color = 'w3-selection-green';
1510var w3_selection_green = '#4CAF50';
1511
1512function w3_highlight(el_id)
1513{
1514	var el = w3_el(el_id);
1515	//console.log('w3_highlight '+ el.id);
1516	w3_add(el, el.w3int_highlight_color || w3_highlight_color);
1517}
1518
1519function w3_unhighlight(el_id)
1520{
1521	var el = w3_el(el_id);
1522	//console.log('w3_unhighlight '+ el.id);
1523	w3_remove(el, el.w3int_highlight_color || w3_highlight_color);
1524}
1525
1526function w3_isHighlighted(el_id)
1527{
1528	var el = w3_el(el_id);
1529	return w3_contains(el, el.w3int_highlight_color || w3_highlight_color);
1530}
1531
1532function w3_schedule_highlight(el_id)
1533{
1534   var el = w3_el(el_id);
1535   w3_highlight(el);
1536   setTimeout(function() { w3_unhighlight(el); }, w3_highlight_time);
1537}
1538
1539function w3_set_highlight_color(el_id, color)
1540{
1541	var el = w3_el(el_id);
1542	if (!el) return null;
1543
1544	if (w3_isHighlighted(el)) {
1545	   w3_unhighlight(el);
1546	   el.w3int_highlight_color = color;
1547	   w3_highlight(el);
1548	} else {
1549	   el.w3int_highlight_color = color;
1550	}
1551
1552	return el;
1553}
1554
1555var w3_flag_color = 'w3-override-yellow';
1556
1557function w3_flag(path, s)
1558{
1559   var el = w3_el(path);
1560   if (isString(s)) w3_innerHTML(el, s);
1561	w3_add(el, w3_flag_color);
1562}
1563
1564function w3_unflag(path, s)
1565{
1566   var el = w3_el(path);
1567   if (isString(s)) w3_innerHTML(el, s);
1568	w3_remove(el, w3_flag_color);
1569}
1570
1571function w3_flag_cond(path, cond, s)
1572{
1573   var el = w3_el(path);
1574   if (cond)
1575      w3_flag(el, s);
1576   else
1577      w3_unflag(el, s);
1578}
1579
1580// for when you don't want to w3_add(el_id, "[w3-text-color]")
1581// returns previous color
1582function w3_color(el_id, color, bkgColor, cond)
1583{
1584	var el = w3_el(el_id);
1585	if (!el) return null;
1586	var prev_fg = el.style.color;
1587	var prev_bg = el.style.backgroundColor;
1588	
1589	// allow w3_color(el_id, [color, bkgColor], cond)
1590	if (isArray(color)) {
1591	   var a = color;
1592	   color = a[0];
1593	   cond = bkgColor;
1594	   bkgColor = a[1];
1595	}
1596	
1597	// remember that setting colors to '' restores default
1598   cond = (isUndefined(cond) || cond);
1599   if (isArg(color)) el.style.color = cond? color:'';
1600   if (isArg(bkgColor)) el.style.backgroundColor = cond? bkgColor:'';
1601	return { color: prev_fg, backgroundColor: prev_bg };
1602}
1603
1604// returns previous color
1605function w3_background_color(el_id, color)
1606{
1607	var el = w3_el(el_id);
1608	var prev = el.style.backgroundColor;
1609	if (color != undefined && color != null) el.style.backgroundColor = color;
1610	return prev;
1611}
1612
1613// c1 = color if cond false
1614// c2 = color (optional) if cond true/undefined
1615// c[12] =
1616//    'css-color(fg)'
1617//    ['css-color(fg)', 'css-color(bg)']
1618//    'w3-text-color'(fg) or 'w3-color'(bg)
1619// CSS color '' to revert to default color
1620function w3_colors(el_id, c1, c2, cond)
1621{
1622   var el = w3_el(el_id);
1623   if (!el) return null;
1624   cond = isArg(cond)? cond : true;
1625
1626   var fg1 = null, bg1 = null;
1627   if (isArray(c1)) {
1628      if (c1.length > 0) fg1 = c1[0];
1629      if (c1.length > 1) bg1 = c1[1];
1630   } else
1631   if (isString(c1)) {
1632      fg1 = c1;
1633   } else {
1634      return null;
1635   }
1636   
1637   //console.log('w3_colors fg1='+ fg1 +' '+ cond);
1638   if (fg1.startsWith('w3-')) {
1639      w3_remove(el, fg1);
1640      if (!cond) w3_add(el, fg1);
1641   } else {
1642         if (!cond) el.style.color = fg1;
1643   }
1644   if (bg1) {
1645      el.style.backgroundColor = bg1;
1646   }
1647
1648   var fg2 = null, bg2 = null;
1649   if (isArray(c2)) {
1650      if (c2.length > 0) fg2 = c2[0];
1651      if (c2.length > 1) bg2 = c2[1];
1652   } else
1653   if (isString(c2)) {
1654      fg2 = c2;
1655   } else {
1656      return null;
1657   }
1658   
1659   //console.log('w3_colors fg2='+ fg2 +' '+ cond);
1660   if (fg2.startsWith('w3-')) {
1661      w3_remove(el, fg2);
1662      if (cond) w3_add(el, fg2);
1663   } else {
1664         if (cond) el.style.color = fg2;
1665   }
1666   if (bg2) {
1667      el.style.backgroundColor = bg2;
1668   }
1669}
1670
1671function w3_flip_colors(el_id, colors, cond)
1672{
1673   var el = w3_el(el_id);
1674   if (!el) return null;
1675   var ar = colors? colors.split(' ') : null;
1676   if (!ar || ar.length != 2) return null;
1677   w3_remove(el, ar[0]);
1678   w3_remove(el, ar[1]);
1679   w3_add(el, ar[cond? 1:0]);
1680}
1681
1682function w3_fg_color_with_opacity_against_bk_color(fg, opacity, bg)
1683{
1684   var fg_color = w3color(fg);
1685   if (!fg_color.valid) return null;
1686   var bg_color = w3color(bg);
1687   if (!bg_color.valid) return null;
1688   var _opacity = 1 - opacity;
1689   var red    = Math.round(opacity * fg_color.red   + _opacity * bg_color.red);
1690   var green  = Math.round(opacity * fg_color.green + _opacity * bg_color.green);
1691   var blue   = Math.round(opacity * fg_color.blue  + _opacity * bg_color.blue);
1692   return w3color(kiwi_rgb(red, green, blue));
1693}
1694
1695function w3_opacity(el_id, opacity)
1696{
1697   var el = w3_el(el_id);
1698   if (!el) return null;
1699   el.style.opacity = opacity;
1700}
1701
1702function w3_flash_fade(el_id, color, dwell_ms, fade_ms, color2)
1703{
1704   var el = w3_el(el_id);
1705   if (!el) return el;
1706   el.style.transition = 'background 0s';    // has to be "0s" and not just "0"
1707   el.style.background = color;
1708   setTimeout(function() {
1709      el.style.transition = 'background '+ (fade_ms? (fade_ms/1000) : 0.3) +'s linear';
1710      el.style.background = isString(color2)? color2 : 'black';
1711   }, dwell_ms? dwell_ms : 50);
1712   return el;
1713}
1714
1715function w3_fillText_shadow(canvas, text, x, y, font, fontSize, color, opts) {
1716   var strokeWidth = w3_opt(opts, 'stroke', Math.floor(fontSize / 3));  // rule of thumb for good looking shadow
1717
1718   var ctx = canvas.getContext("2d");
1719   ctx.font = px(fontSize) +' '+ font;
1720   ctx.miterLimit = 2;
1721   ctx.lineJoin = "circle";
1722   var tw = ctx.measureText(text).width;
1723   if (!w3_opt(opts, 'left')) x -= tw/2;     // i.e. centered
1724
1725   ctx.strokeStyle = "black";
1726   ctx.lineWidth = strokeWidth;
1727   ctx.strokeText(text, x, y);
1728
1729   ctx.fillStyle = color;
1730   ctx.lineWidth = 1;
1731   ctx.fillText(text, x, y);
1732}
1733
1734function w3_fillText(ctx, x, y, text, color, opts)
1735{
1736   color = color || 'black';                    ctx.fillStyle = color;
1737   var font = w3_opt(opts, 'font');             if (isDefined(font)) ctx.font = font;
1738   var lineWidth = w3_opt(opts, 'lineWidth');   if (isDefined(lineWidth)) ctx.lineWidth = lineWidth;
1739   var align = w3_opt(opts, 'align');           if (isDefined(align)) ctx.textAlign = align;
1740   var baseline = w3_opt(opts, 'baseline');     if (isDefined(baseline)) ctx.textBaseline = baseline;
1741
1742   var tw = ctx.measureText(text).width;
1743   if (!w3_opt(opts, 'left')) x -= tw/2;     // i.e. centered
1744   ctx.fillText(text, x, y);
1745}
1746
1747function w3_check_restart_reboot(el_id)
1748{
1749	var el = w3_el(el_id);
1750   if (!el) return;
1751	
1752	w3_iterate_parent(el, function(el) {
1753		if (w3_contains(el, 'w3-restart')) {
1754			w3_restart_cb();
1755			return el;
1756		}
1757		if (w3_contains(el, 'w3-reboot')) {
1758			w3_reboot_cb();
1759			return el;
1760		}
1761		return null;
1762	});
1763}
1764
1765// XXX just an idea -- not working yet
1766function w3_check_cfg_reload(el_id)
1767{
1768   if (!isAdmin()) return;
1769	var el = w3_el(el_id);
1770   if (!el || !w3_contains(el, 'w3-cfg-restart')) return;
1771
1772   kiwi_clearTimeout(w3int.reload_timeout);
1773   w3int.reload_timeout = setTimeout(
1774      function() {
1775         w3int.update_seq++;
1776         ext_send('ADM reload_cfg');
1777         console.log('w3 reload_cfg');
1778         
1779         /* XXX this needs to be done in the context of each user connection
1780         setTimeout(
1781            function() {
1782               if (ext_name()) w3_call(ext_name() +'_cfg_reload');
1783               w3_call('ant_switch_cfg_reload');
1784         }, 5000);
1785         */
1786      }, 5000);
1787}
1788
1789function w3_set_value(path, val)
1790{
1791   //console.log('w3_set_value path='+ path +' val='+ val);
1792   path = w3_add_id(path);
1793   if (1) {
1794      // set all similarly named elements
1795      w3_els(path, function(el) {
1796         if (el) el.value = val;
1797      });
1798   } else {
1799      var el = w3_el(path);
1800   /*
1801      if (val == '(encrypted)' && w3_contains(el, 'w3-encrypted'))
1802         w3_add(el, 'kiwi-pw');
1803   */
1804      if (el) el.value = val;
1805   }
1806}
1807
1808function w3_set_decoded_value(path, val)
1809{
1810	//console.log('w3_set_decoded_value: path='+ path +' val='+ val);
1811	w3_set_value(path, kiwi_decodeURIComponent('w3_set_decoded_value:'+ path, val));
1812}
1813
1814function w3_get_value(path)
1815{
1816	var el = w3_el(path);
1817	if (!el) return null;
1818	return el.value;
1819}
1820
1821function w3_placeholder(path, val)
1822{
1823	var el = w3_el(path);
1824	if (!el) return null;
1825   w3_set_value(el, '');
1826   w3_attribute(el, 'placeholder', val);
1827}
1828
1829function w3_add_toplevel(path)
1830{
1831	if (!path.startsWith('cfg.') && !path.startsWith('adm.'))
1832		return 'cfg.'+ path;
1833	return path;
1834}
1835
1836function w3_not_toplevel(path)
1837{
1838	if (path.startsWith('cfg.') || path.startsWith('adm.'))
1839		return path.substr(path.indexOf('.') + 1);
1840	return path;
1841}
1842
1843function w3_basename(path)
1844{
1845	var i = path.lastIndexOf('.');
1846	if (i >= 0) {
1847		return path.substr(i+1);
1848	}
1849	return path;
1850}
1851
1852// add 'c' between string arguments
1853function w3_sbc(c)
1854{
1855   var a = [];
1856   var strnum = function(s) {
1857      s = isString(s)? s.trim() : (isNumber(s)? s.toString() : '');
1858      if (s != '') a.push(s);
1859   };
1860   for (var i = 1; i < arguments.length; i++) {
1861      var s = arguments[i];
1862      if (isObject(s)) {   // e.g. from w3_sb(), w3_cat() below
1863         w3_obj_enum(s, function(k,i,o) {
1864            strnum(o);
1865         });
1866      } else {
1867         strnum(s);
1868      }
1869   }
1870	return a.length? a.join(c) : '';
1871}
1872
1873function w3_sb() { return w3_sbc(' ', arguments); }
1874
1875function w3_cat() { return w3_sbc('', arguments); }
1876
1877// "space between ending with"
1878function w3_sbew(ew)
1879{
1880   var a = [];
1881   for (var i = 1; i < arguments.length; i++) {
1882      var s = arguments[i];
1883      s = isString(s)? s.trim() : (isNumber(s)? s.toString() : '');
1884      if (s != '') {
1885         if (s[s.length-1] != ew) s = s + ew;
1886         a.push(s);
1887      }
1888   }
1889	return a.length? a.join(' ') : '';
1890}
1891
1892// append \n if s is not empty
1893function w3_nl(s)
1894{
1895   if (isArg(s) && s != '') s += '\n';
1896   return s;
1897}
1898
1899// append <br> if s is not empty
1900function w3_br(s)
1901{
1902   if (isArg(s) && s != '') s += '<br>';
1903   return s;
1904}
1905
1906// psa = prop|style|attr
1907// => [class="[prop] [extra_prop]"] [style="[style] [extra_style]"] [attr] [extra_attr]
1908function w3_psa(psa, extra_prop, extra_style, extra_attr)
1909{
1910   var dump = psa && psa.includes('w3-dump');
1911   if (dump) {
1912	   console.log('psa_in=['+ psa +']');
1913	   console.log('extra_prop=['+ extra_prop +']');
1914	   console.log('extra_style=['+ extra_style +']');
1915	   console.log('extra_attr=['+ extra_attr +']');
1916	}
1917
1918   if (psa && psa.startsWith('class=')) {
1919      //console.log('#### w3_psa RECURSION ####');
1920      return psa;    // already processed
1921   }
1922   
1923	var a = psa? psa.split('|') : [];
1924	psa = '';
1925
1926	var prop = a[0] || '';
1927	if (extra_prop) prop = w3_sb(prop, extra_prop);
1928	if (prop != '') psa = 'class='+ dq(prop);
1929
1930	var style = a[1] || '';
1931	if (extra_style) style = w3_sbew(';', style, extra_style);
1932	if (style != '') psa = w3_sb(psa, 'style='+ dq(style));
1933
1934	var attr = (a[2] || '').replace(/&vbar;/g, '|');   // e.g. title="foo&vbar;bar" => title="foo|bar"
1935	if (extra_attr) attr = w3_sb(attr, extra_attr);
1936	if (attr != '') psa = w3_sb(psa, attr);
1937
1938	if (dump) console.log('psa_out=['+ psa +']');
1939	return psa;
1940}
1941
1942// given as input:
1943//    'right'
1944//    'middle/right'
1945//    'left/middle/right'
1946// returns:
1947//    { left:'...', middle:'...', right:'...', num:[0123] }
1948function w3_psa3(psa3)
1949{
1950   //if (psa3.includes('w3-dump')) console.log('w3_psa3 in=['+ psa3 +']');
1951   psa3 = psa3 || '';
1952
1953	var a = psa3.split('||');
1954	if (a.length == 2) {
1955	   if (a[1].includes('/')) {
1956	      var s = a[1].replace(/\//g, '&slash;');
1957	      // e.g. ...||... title="foo/bar instead of foo&slash;bar"
1958	      console.log('w3_psa3 psa='+ sq(psa3));
1959	      console.log('w3_psa3 DANGER: psa attribute part after "||" contained "/", replaced with "&slash;"');
1960	      console.log("w3_psa3 before: '||"+ a[1] +"'");
1961	      console.log("w3_psa3  after: '||"+ s +"'");
1962	      psa3 = a[0] +'||'+ s;
1963	   }
1964	}
1965
1966   a = psa3.split('/');
1967   var a0 = (a[0] || '').replace(/&slash;/g, '/');
1968   var a1 = (a[1] || '').replace(/&slash;/g, '/');
1969   var a2 = (a[2] || '').replace(/&slash;/g, '/');
1970   //if (psa3.includes('w3-dump')) console.log(a);
1971
1972   if (a.length == 1)
1973      return { left:'', middle:'', right:a0, num:1 };
1974   else
1975   if (a.length == 2)
1976      return { left:'', middle:a0, right:a1, num:2 };
1977   else
1978   if (a.length == 3)
1979      return { left:a0, middle:a1, right:a2, num:3 };
1980   else
1981      return { left:'', middle:'', right:'', num:0 };
1982}
1983
1984// like w3_psa() except returns in original psa format (i.e. not expanded to "class=...")
1985// NB: does not handle left/middle/right notation -- only p|s|a
1986function w3_psa_mix(psa, extra_prop, extra_style, extra_attr)
1987{
1988   var dump = psa.includes('w3-dump');
1989   if (dump) console.log('--- w3_psa_mix psa='+ psa);
1990	var a = psa? psa.split('|') : [];
1991
1992	var prop = a[0] || '';
1993   if (dump) console.log('prop=['+ prop +']');
1994   if (dump) console.log('extra_prop=['+ extra_prop +']');
1995	if (extra_prop) prop = w3_sb(prop, extra_prop);
1996   if (dump) console.log('mixed prop=['+ prop +']');
1997
1998	var style = a[1] || '';
1999   if (dump) console.log('style=['+ style +']');
2000   if (dump) console.log('extra_style=['+ extra_style +']');
2001	if (extra_style) style = w3_sbew(';', style, extra_style);
2002   if (dump) console.log('mixed style=['+ style +']');
2003
2004	var attr = a[2] || '';     // NB: don't do &vbar; => | expansion here since output is in psa format
2005   if (dump) console.log('attr=['+ attr +']');
2006   if (dump) console.log('extra_attr=['+ extra_attr +']');
2007	if (extra_attr) attr = w3_sb(attr, extra_attr);
2008   if (dump) console.log('mixed attr=['+ attr +']');
2009
2010   psa = prop +'|'+ style +'|'+ attr;
2011	if (dump) console.log('mix_out=['+ psa +']');
2012	return psa;
2013}
2014
2015function w3_set_psa(el, psa)
2016{
2017   el = w3_el(el);
2018   if (!el || isEmptyString(psa)) return null;
2019   psa = psa.split('/')[0];
2020   psa = psa.split('|');
2021   var props = psa[0];
2022   var styles = psa[1];
2023   var attrs = psa[2];
2024   if (isArg(props)) w3_add(el, props);
2025   if (isArg(styles)) el.style = styles;
2026   if (isArg(attrs)) w3_set_attributes_from_string(el, attrs);
2027}
2028
2029function w3int_init()
2030{
2031}
2032
2033function w3int_post_action()
2034{
2035   // if it exists, re-select the main page frequency field
2036   //console.log('w3int_post_action freqset_select');
2037   w3_call('freqset_select');
2038}
2039
2040function w3_isScrollingY(id)
2041{
2042   var el = w3_el(id);
2043   if (!el) return null;
2044   return (el.scrollHeight > el.clientHeight);
2045}
2046
2047function w3_isScrollingX(id)
2048{
2049   var el = w3_el(id);
2050   if (!el) return null;
2051   return (el.scrollWidth > el.clientWidth);
2052}
2053
2054// see: developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
2055function w3_isScrolledDown(id)
2056{
2057   var el = w3_el(id);
2058   if (!el) return null;
2059   return (Math.abs(el.scrollHeight - el.clientHeight - el.scrollTop) < 1);
2060}
2061
2062// returns 0 to 1.0 (-1 on error)
2063function w3_scrolledPosition(id)
2064{
2065   var el = w3_el(id);
2066   if (!el) return null;
2067   //console.log('w3_scrolledPosition scrollTop='+ el.scrollTop +' scrollHeight='+ el.scrollHeight +' clientHeight='+ el.clientHeight);
2068   if (el.clientHeight == 0) return -1;      // element hidden most likely
2069   var scrollH = el.scrollHeight - el.clientHeight;
2070   if (scrollH <= 0) return 0;      // no scrollbar, so always at top
2071   return el.scrollTop / scrollH;
2072}
2073
2074function w3_scrollTo(id, pos, jog)
2075{
2076   var el = w3_el(id);
2077   if (!el) return null;
2078   var scrollH = el.scrollHeight - el.clientHeight;
2079   if (scrollH < 0) scrollH = 0;    // no scrollbar, so always at top
2080   var top = scrollH * pos;
2081   var unchanged = (top == el.scrollTop);
2082   //console.log('w3_scrollTo: unchanged='+ unchanged +' pos='+ pos +' sH='+ el.scrollHeight +' cH='+ el.clientHeight +' top='+ top +' scrollTop='+ el.scrollTop);
2083   el.scrollTop = top;
2084   
2085   // jog to force scroll event
2086   if (jog == true) {
2087      el.scrollTop = top+1;
2088      el.scrollTop = top;
2089   }
2090   return unchanged;
2091}
2092
2093// id element must have scroll property
2094function w3_scrollDown(id, cond)
2095{
2096   var el = w3_el(id);
2097   if (isArg(cond)? cond : true) el.scrollTop = el.scrollHeight;
2098   return el;
2099}
2100
2101// id element must have scroll property
2102function w3_scrollTop(id, cond)
2103{
2104   var el = w3_el(id);
2105   if (isArg(cond)? cond : true) el.scrollTop = 0;
2106   return el;
2107}
2108
2109// calls func(arg) when w3_el(id) exists in DOM
2110function w3_do_when_rendered(el_id, func, arg, poll_ms)
2111{
2112   w3_do_when_cond(
2113      function() { return isArg(w3_el(el_id)); },
2114      func, arg, poll_ms
2115   );
2116}
2117
2118// calls func(arg) when w3_el(id).clientHeight is non-zero
2119function w3_do_when_clientHeight(el_id, func, arg, poll_ms)
2120{
2121   w3_do_when_cond(
2122      function() {
2123         var el = w3_el(el_id);
2124         if (!el) return false;
2125         return (el.clientHeight != 0);
2126      }, func, arg, poll_ms
2127   );
2128}
2129
2130// calls func(arg) when cond_func() becomes true
2131function w3_do_when_cond(cond_func, func, arg, poll_ms)
2132{
2133   poll_ms = isNumberElse(poll_ms, 100);
2134   if (!cond_func(arg)) {
2135      setTimeout(function () {
2136         w3_do_when_cond(cond_func, func, arg, poll_ms);
2137      }, poll_ms);
2138   } else {
2139      w3_call(func, arg);
2140   }
2141   
2142   // REMINDER: Other than the FIRST call, returns from here don't go anywhere.
2143   // In particular the original caller is returned to:
2144   //    1) If the FIRST check of cond_func() is true and after func() runs.
2145   //    2) If the FIRST check of cond_func() is false and after the first setTimeout() runs.
2146   // But the caller is NOT suspended or somehow waiting for a delayed run of func() when
2147   // conf_func() is false for some number of setTimeout() periods.
2148   // That's not how javascript works (Web Workers aside). It is event driven and all threads
2149   // must complete without conditional delay.
2150}
2151
2152// w3_elementAtPointer(evt)
2153// w3_elementAtPointer(x, y)
2154function w3_elementAtPointer(x, y)
2155{
2156   if (isObject(x)) {
2157      var evt = x;
2158      x = evt.pageX; y = evt.pageY;
2159   }
2160   if (isNumber(x) && isNumber(y)) {
2161      return document.elementFromPoint(x,y);
2162   }
2163   return null;
2164}
2165
2166function w3_event_log(el, id, event_name)
2167{
2168   el = w3_el(el);
2169   if (el == null) return;
2170   el.addEventListener(event_name,
2171      function() {
2172         console.log('w3_event_log '+ id +': '+ event_name);
2173         //canvas_log(event_name);
2174      }, w3.BUBBLING
2175   );
2176}
2177
2178// observe element event behavior
2179function w3_event_listener(id, el)
2180{
2181   el = w3_el(el);
2182   if (!el) return null;
2183   if (!w3int.eventListener[id]) {
2184      w3_event_log(el, id, 'mouseup');
2185      w3_event_log(el, id, 'mousedown');
2186      w3_event_log(el, id, 'click');
2187      w3_event_log(el, id, 'change');
2188      w3_event_log(el, id, 'submit');
2189      w3_event_log(el, id, 'focus');
2190      w3_event_log(el, id, 'blur');
2191      w3int.eventListener[id] = true;
2192   }
2193}
2194
2195// Return canvas-relative mouse x/y for a canvas potentially scrolling due to overflow.
2196// Mouse x/y in canvas CSS pixels (handles DPI / CSS scaling).
2197function w3_canvas_event_xy(evt, canvas)
2198{
2199   var el = w3_el(canvas);
2200   var rect = el.getBoundingClientRect();
2201   var sx = rect.width? (el.width / rect.width) : 1;
2202   var sy = rect.height? (el.height / rect.height) : 1;
2203   var x, y;
2204   if (isArg(evt.offsetX) && isArg(evt.offsetY) && evt.target === el) {
2205      x = evt.offsetX * sx;
2206      y = evt.offsetY * sy;
2207      //console.log('used offsetXY');
2208   } else {
2209      x = (evt.pageX - rect.left) * sx;
2210      y = (evt.pageY - rect.top) * sy;
2211      //console.log('used pageXY');
2212   }
2213   //console.log({ t:'w3_canvas_event_xy', sx:sx, sy:sy, x:x, y:y });
2214   return { x:x, y:y, rect:rect };
2215}
2216
2217function w3_xy_within(start, stop, delta)
2218{
2219   //console.log('w3_xy_within');
2220   //console.log(start);
2221   //console.log(stop);
2222   var rv = 
2223      (Math.abs(stop.x - start.x) <= delta) &&
2224      (Math.abs(stop.y - start.y) <= delta);
2225   //console.log('rv='+ rv);
2226   return rv;
2227}
2228
2229
2230////////////////////////////////
2231// hr
2232////////////////////////////////
2233
2234function w3_hr(psa)
2235{
2236   var p = w3_psa(psa);
2237	var s = '<hr '+ p +'>';
2238	var narg = arguments.length;
2239		for (var i=1; i < narg; i++) {
2240			s += arguments[i];
2241		}
2242	//console.log(s);
2243	return s;
2244}
2245
2246
2247////////////////////////////////
2248// nav
2249////////////////////////////////
2250
2251function w3_click_nav(nav_id, next_id, cb_next, cb_param, from)
2252{
2253   //console.log('--------');
2254   //console.log('w3_click_nav START nav_id='+ nav_id +' next_id='+ next_id +' cb_next='+ cb_next +' cb_param='+ cb_param +' from='+ from);
2255	var next_id_nav = ('id-nav-'+ next_id).toLowerCase();    // to differentiate the nav anchor from the nav container
2256	var cur_id = null;
2257	var next_el = null;
2258   var cb_prev = null;
2259   var found = 0;
2260
2261   var el = w3_el(nav_id);
2262   //console.log('w3_click_nav nav_id='+ nav_id +' el='+ el);
2263   if (!el) return;
2264   
2265	w3_iterate_children(el, function(el, i) {
2266	   //console.log('w3_click_nav consider: nodename='+ el.nodeName);
2267	   if (el.nodeName != 'A' || found >= 2) return;
2268	   //if (el.nodeName != 'DIV') return;
2269
2270		//console.log('w3_click_nav consider: id=('+ el.id +') ==? next_id=('+ next_id_nav +') el.className="'+ el.className +'"');
2271		if (w3_contains(el, 'w3int-cur-sel')) {
2272			cur_id = el.id.substring(7);		// remove 'id-nav-' added by w3int_anchor()
2273
2274		   //console.log('w3_click_nav FOUND-CUR cur_id='+ cur_id);
2275			w3_remove(el, 'w3int-cur-sel');
2276			w3_iterate_classList(el, function(s, i) {
2277			   if (s.startsWith('id-nav-cb-'))
2278			      cb_prev = s.substring(10);
2279			});
2280			found++;
2281		}
2282
2283		if (el.id.toLowerCase().startsWith(next_id_nav)) {
2284		   //console.log('w3_click_nav FOUND-NEXT id='+ next_id);
2285		   next_id = el.id.split('id-nav-')[1];
2286			next_el = el;
2287			found++;
2288		}
2289	});
2290
2291   // toggle visibility of current content
2292	if (cur_id && cb_prev) {
2293		//console.log('w3_click_nav BLUR cb_prev='+ cb_prev +' cur_id='+ cur_id);
2294		w3_call(cb_prev +'_blur', cur_id);
2295	}
2296	if (cur_id)
2297		w3_toggle(cur_id, 'w3-show-block');
2298
2299   // make new nav item current and visible / focused
2300	if (next_el) {
2301		w3_add(next_el, 'w3int-cur-sel');
2302	   w3_check_restart_reboot(next_el);
2303	}
2304
2305	w3_toggle(next_id, 'w3-show-block');
2306	if (cb_next != 'null') {
2307	   //console.log('w3_click_nav FOCUS cb_next='+ cb_next +' next_id='+ next_id);
2308      w3_call(cb_next +'_focus', next_id, cb_param);
2309   }
2310	//console.log('w3_click_nav DONE cb_prev='+ cb_prev +' cur_id='+ cur_id +' cb_next='+ cb_next +' next_id='+ next_id);
2311}
2312
2313// id = unique, cb = undefined => cb = id
2314// id = unique, cb = func
2315// id = unique, cb = null => don't want focus/blur callbacks
2316function w3int_anchor(psa, text, navbar, id, cb, isSelected)
2317{
2318   if (cb === undefined) cb = id;
2319   var nav_cb = cb? ('id-nav-cb-'+ cb) : '';
2320   //console.log('w3int_anchor id='+ id +' cb='+ cb +' nav_cb='+ nav_cb);
2321
2322	// store id prefixed with 'id-nav-' so as not to collide with content container id prefixed with 'id-'
2323	var attr = 'id="id-nav-'+ id +'" onclick="w3_click_nav('+ sq(navbar) +', '+ sq(id) +', '+ sq(cb) +')"';
2324	//console.log('w3int_anchor psa: '+ psa);
2325	//console.log('w3int_anchor attr: '+ attr);
2326   var p = w3_psa(psa, nav_cb + (isSelected? ' w3int-cur-sel':''), '', attr);
2327   //var p = w3_psa(psa, 'w3-show-inline '+ nav_cb + (isSelected? ' w3int-cur-sel':''), '', attr);
2328	//console.log('w3int_anchor p: '+ p);
2329	
2330	// store with id= instead of a class property so it's easy to find with el.id in w3_iterate_classname()
2331	// href of "javascript:void(0)" instead of "#" so page doesn't scroll to top on click
2332	var s = '<a '+ p +' href="javascript:void(0)">'+ text +'</a>';
2333   //var s = w3_div(p, text);
2334	//console.log('w3int_anchor: '+ s);
2335	return s;
2336}
2337
2338function w3_navbar(psa)
2339{
2340   var p = w3_psa(psa, 'w3-navbar');
2341	var s = '<nav '+ p +'>';
2342	var narg = arguments.length;
2343		for (var i=1; i < narg; i++) {
2344			s += arguments[i];
2345		}
2346	s += '</nav>';
2347	//console.log(s);
2348	return s;
2349}
2350
2351function w3_sidenav(psa)
2352{
2353   var p = w3_psa(psa, 'w3-sidenav w3-static w3-left w3-sidenav-full-height w3-light-grey');
2354	var s = '<nav '+ p +'>';
2355	var narg = arguments.length;
2356		for (var i=1; i < narg; i++) {
2357			s += arguments[i];
2358		}
2359	s += '</nav>';
2360	//console.log(s);
2361	return s;
2362}
2363
2364function w3_nav(psa, text, navbar, id, cb, isSelected)
2365{
2366	return w3int_anchor(psa, text, navbar, id, cb, isSelected);
2367}
2368
2369function w3_navdef(psa, text, navbar, id, cb)
2370{
2371	// must wait until instantiated before manipulating 
2372	setTimeout(function() {
2373		//console.log('w3_navdef instantiate focus');
2374		w3_toggle(id, 'w3-show-block');
2375	}, w3_highlight_time);
2376	
2377	return w3int_anchor(psa, text, navbar, id, cb, true);
2378}
2379
2380
2381////////////////////////////////
2382// labels
2383////////////////////////////////
2384
2385function w3int_label(psa, text, path, extension)
2386{
2387   psa = psa || '';
2388   text = text || '';
2389   extension = extension || '';
2390   var dump = psa.includes('w3-dump');
2391
2392   // don't emit empty label div
2393   //if (psa == '' && text == '' && extension == '') return '';
2394   if (text == '' && ((psa != '' && psa != '||') || extension != '')) {
2395      console.log('$w3int_label', {text, psa, extension});
2396      //kiwi_trace();
2397   }
2398   if (text == '') {
2399      if (dump) console.log('$w3int_label <empty string>');
2400      return '';
2401   }
2402   
2403   // most likely already an embedded w3_label()
2404   if (text.startsWith('<label ')) {
2405      if (dump) console.log('$w3int_label EMBEDDED '+ text);
2406      return text;
2407   }
2408   
2409   var id = w3_add_id(path, '-label');    // so w3_set_label() can find label
2410	//var inline = psa.includes('w3-label-inline');
2411	var p = w3_psa(psa, id);
2412	var s = '<label '+ p +'>'+ text + extension +'</label>';
2413	//var s = '<label '+ p +'>'+ text + extension + (inline? '':'<br>') +'</label>';
2414	if (dump) console.log('$w3int_label psa='+ psa +' text=<'+ text +'> s=<'+ s +'>');
2415	return s;
2416}
2417
2418// if called directly always emit label div even if text is empty
2419function w3_label(psa, text, path, extension)
2420{
2421   if (arguments.length >= 4) {
2422      console.log('### w3_label ext='+ extension);
2423      //kiwi_trace();
2424   }
2425   if (isEmptyString(text)) text = '&nbsp;';
2426   return w3int_label(psa, text, path, extension);
2427}
2428
2429function w3_get_label(label, path)
2430{
2431	return w3_get_innerHTML(w3_add_id(path, '-label'));
2432}
2433
2434function w3_set_label(label, path)
2435{
2436	w3_innerHTML(w3_add_id(path, '-label'), label);
2437}
2438
2439
2440////////////////////////////////
2441// title
2442////////////////////////////////
2443
2444function w3_title(id, text)
2445{
2446   //console.log('w3_title id='+ id +' text='+ text);
2447   var el = w3_el(id);
2448   if (!el) return null;
2449   w3_attribute(el, 'title', text);
2450   return el;
2451}
2452
2453function w3_title_multi(id, text)
2454{
2455   w3_els(id, function(el, i) { el.title = text; } );
2456}
2457
2458
2459////////////////////////////////
2460// link
2461////////////////////////////////
2462
2463function w3int_link_click(ev, cb, cb_param)
2464{
2465   //console.log('w3int_link_click cb='+ cb +' cb_param='+ cb_param);
2466   //console.log(ev);
2467   var el = ev.currentTarget;
2468   //console.log(el);
2469   w3_check_restart_reboot(el);
2470
2471   // cb is a string because can't pass an object to onclick
2472   if (cb) {
2473      w3_call(cb, el, cb_param, /* first */ false);   // links don't have first callback
2474   }
2475
2476   w3int_post_action();
2477}
2478
2479function w3_link(psa, url, inner, title, cb, cb_param)
2480{
2481   var qual_url = url;
2482   var href = '';
2483   var target = '';
2484   if (url != '') {
2485      if (!url.startsWith('javascript:')) {
2486         if (!url.startsWith('http://') && !url.startsWith('https://'))
2487            qual_url = 'http://'+ url;
2488         target = ' target="_blank"';
2489      }
2490      href = 'href='+ dq(qual_url) +' ';
2491   }
2492   inner = inner || '';
2493   title = title || '';
2494   if (title != '') title = ' title='+ dq(title);
2495
2496   // by default use pointer cursor if there is a callback
2497	var pointer = (cb && cb != '')? 'w3-pointer':'';
2498	cb_param = cb_param || 0;
2499	var onclick = cb? (' onclick="w3int_link_click(event, '+ sq(cb) +', '+ sq(cb_param) +')"') : '';
2500
2501	var p = w3_psa(psa, pointer, '', href + target + title + onclick);
2502
2503   // escape HTML '<' and '>' so they work with kiwi_output_msg()
2504	var esc_html = psa.includes('w3-esc-html');
2505	var lt = esc_html? kiwi.esc_lt : '<';
2506	var gt = esc_html? kiwi.esc_gt : '>';
2507	var s = lt +'a '+ p + gt + inner + lt +'/a'+ gt;
2508	//console.log(s);
2509	return s;
2510}
2511
2512function w3_link_set(path, url)
2513{
2514   var el = w3_el(path);
2515   //console.log('w3_link_set: path='+ path +' url='+ url);
2516   //console.log(el);
2517   if (el && url != '') {
2518      if (!url.startsWith('http://') && !url.startsWith('https://'))
2519         url = 'http://'+ url;
2520      w3_attribute(el, 'href', url);
2521      w3_attribute(el, 'target', '_blank');
2522   }
2523}
2524
2525
2526////////////////////////////////
2527// image
2528////////////////////////////////
2529
2530function w3_img_wh(src, func)
2531{
2532   // see: stackoverflow.com/questions/318630/get-the-real-width-and-height-of-an-image-with-javascript-in-safari-chrome
2533   // There are huge issues obtaining the true height/width of an img given variable state during loading (caching etc).
2534   var img = new Image();
2535   img.onload = function() {
2536      //console.log('w3_img_wh '+ this.width +'x'+ this.height +' src='+ src);
2537      func(this.width, this.height);
2538   };
2539   img.src = src;
2540}
2541
2542function w3_img(psa, file, width, height)
2543{
2544   var w = isArg(width)? ('width='+ width) : '';
2545   var h = isArg(height)? ('height='+ height) : '';
2546   var src = 'src='+ dq(file);
2547	var p = w3_psa(psa, null, null, w3_sb(src, w, h));
2548	var s = '<img '+ p +' />';
2549	if (psa && psa.includes('w3-dump')) console.log('w3-dump: '+ s);
2550	return s;
2551}
2552
2553
2554////////////////////////////////
2555// buttons: radio
2556////////////////////////////////
2557
2558var w3_SELECTED = true;
2559var w3_NOT_SELECTED = false;
2560
2561function w3_radio_unhighlight(path)
2562{
2563	w3_iterate_classname(w3_add_id(path), function(el) { w3_unhighlight(el); });
2564}
2565
2566function w3int_radio_click(ev, path, cb, cb_param)
2567{
2568   //console.log('w3int_radio_click path='+ path +' cb='+ cb +' cb_param='+ cb_param);
2569	w3_radio_unhighlight(path);
2570	w3_highlight(ev.currentTarget);
2571
2572	var idx = -1;
2573	w3_iterate_classname(w3_add_id(path), function(el, i) {
2574		if (w3_isHighlighted(el))
2575			idx = i;
2576		//console.log('w3int_radio_click CONSIDER path='+ path +' el='+ el +' idx='+ idx);
2577	});
2578
2579	w3_check_restart_reboot(ev.currentTarget);
2580
2581	// cb is a string because can't pass an object to onclick
2582	if (cb) {
2583		w3_call(cb, path, idx, /* first */ false, cb_param);   // radio buttons don't have first callback
2584	}
2585
2586   w3int_post_action();
2587}
2588
2589function w3_radio_button(psa, text, path, isSelected, cb, cb_param)
2590{
2591	cb_param = cb_param || 0;
2592	var onclick = cb? ('onclick="w3int_radio_click(event, '+ sq(path) +', '+ sq(cb) +', '+ sq(cb_param) +')"') : '';
2593	var p = w3_psa(psa, w3_add_id(path) + (isSelected? (' '+ w3_highlight_color) : '') +' w3-btn w3-ext-btn', '', onclick);
2594	var s = '<button '+ p +'>'+ text +'</button>';
2595	//console.log(s);
2596	return s;
2597}
2598
2599// used when current value should come from config param
2600function w3_radio_button_get_param(psa, text, path, selected_if_val, init_val, cb, cb_param)
2601{
2602   // FIXME
2603   //var update_path_var = psa.includes('w3-update');
2604   var save = psa.includes('w3-defer')? EXT_SAVE_DEFER : undefined;
2605	//console.log('w3_radio_button_get_param: '+ path);
2606	var cur_val = ext_get_cfg_param(path, (init_val == undefined)? null : init_val, save);
2607	
2608	// set default selection of button based on current value
2609	var isSelected = (cur_val == selected_if_val)? w3_SELECTED : w3_NOT_SELECTED;
2610	return w3_radio_button(psa, text, path, isSelected, cb, cb_param);
2611}
2612
2613
2614////////////////////////////////
2615// switch: two button switch
2616////////////////////////////////
2617
2618var w3_SWITCH_YES_IDX = 0, w3_SWITCH_NO_IDX = 1;
2619
2620function w3_switch_s(idx) { return ((idx == w3_SWITCH_YES_IDX)? 'YES' : 'NO'); }
2621
2622function w3_switch_val2idx(val) { return (val? w3_SWITCH_YES_IDX : w3_SWITCH_NO_IDX); }
2623
2624function w3_switch_idx2val(idx) { return ((idx == w3_SWITCH_YES_IDX)? 1:0); }
2625
2626function w3_switch(psa, text_0, text_1, path, text_0_selected, cb, cb_param)
2627{
2628   //console.log('w3_switch psa='+ psa);
2629	var s =
2630		w3_radio_button(w3_psa_mix(psa, 'w3int-switch-0'), text_0, path, text_0_selected? 1:0, cb, cb_param) +
2631		w3_radio_button(w3_psa_mix(psa, 'w3int-switch-1'), text_1, path, text_0_selected? 0:1, cb, cb_param);
2632	return s;
2633}
2634
2635function w3_switch_label(psa, label, text_0, text_1, path, text_0_selected, cb, cb_param)
2636{
2637   //console.log('w3_switch_label psa='+ psa);
2638   var hasLabel = (label != '');
2639	var inline = psa.includes('w3-label-inline');
2640	var centered = psa.includes('w3-center');
2641	var left = psa.includes('w3-label-left');
2642	var bold = !psa.includes('w3-label-not-bold');
2643	
2644	var spacing = (hasLabel && !inline)? 'w3-margin-T-8' : '';
2645	if (hasLabel && inline) spacing += left? ' w3-margin-L-8' : ' w3-margin-R-8';
2646	if (centered) spacing += ' w3-halign-center';
2647
2648   var psa3 = w3_psa3(psa);
2649   var psa_outer = w3_psa_mix(psa3.left, (inline? 'w3-inline-flex':'') + (centered? ' w3-center':''));
2650   var psa_label = w3_psa_mix(psa3.middle, (hasLabel && bold)? 'w3-bold':'');
2651	var psa_inner = w3_psa();
2652
2653   var ls = w3int_label(psa_label, label, path);
2654   var cs =
2655      w3_inline(spacing +'/'+ psa3.right,
2656         w3_radio_button('w3int-switch-0', text_0, path, text_0_selected? 1:0, cb, cb_param) +
2657         w3_radio_button('w3int-switch-1', text_1, path, text_0_selected? 0:1, cb, cb_param)
2658      );
2659	var s = w3_div(psa_outer, ((left || !inline)? (ls + cs) : (cs + ls)));
2660	return s;
2661}
2662
2663// used when current value should come from config param
2664function w3_switch_get_param(psa, text_0, text_1, path, text_0_selected_if_val, init_val, cb, cb_param)
2665{
2666   // FIXME
2667   //var update_path_var = psa.includes('w3-update');
2668   var save = psa.includes('w3-defer')? EXT_SAVE_DEFER : undefined;
2669	var cur_val = ext_get_cfg_param(path, (init_val == undefined)? null : init_val, save);
2670
2671	// set default selection of button based on current value
2672	//if (w3_contains(psa, 'id-net-ssl'))
2673	//   console.log('w3_switch_get_param cur_val='+ cur_val +' text_0_selected_if_val='+ text_0_selected_if_val);
2674	var text_0_selected = (cur_val == text_0_selected_if_val)? w3_SELECTED : w3_NOT_SELECTED;
2675   return w3_switch(psa, text_0, text_1, path, text_0_selected, cb, cb_param);
2676}
2677
2678// used when current value should come from config param
2679function w3_switch_label_get_param(psa, label, text_0, text_1, path, text_0_selected_if_val, init_val, cb, cb_param)
2680{
2681   // FIXME
2682   //var update_path_var = psa.includes('w3-update');
2683   var save = psa.includes('w3-defer')? EXT_SAVE_DEFER : undefined;
2684	var cur_val = ext_get_cfg_param(path, (init_val == undefined)? null : init_val, save);
2685
2686	// set default selection of button based on current value
2687	//if (w3_contains(psa, 'id-net-ssl'))
2688	//   console.log('w3_switch_get_param cur_val='+ cur_val +' text_0_selected_if_val='+ text_0_selected_if_val);
2689	var text_0_selected = (cur_val == text_0_selected_if_val)? w3_SELECTED : w3_NOT_SELECTED;
2690   return w3_switch_label(psa, label, text_0, text_1, path, text_0_selected, cb, cb_param);
2691}
2692
2693function w3_switch_set_value(path, switch_idx)
2694{
2695   var sw = 'w3int-switch-'+ switch_idx;
2696   //console.log('w3_switch_set_value: switch='+ sw +' path='+ path);
2697	w3_iterate_classname(path, function(el, i) {
2698      //console.log('w3_switch_set_value: CONSIDER i='+ i);
2699      //console.log(el);
2700		if (w3_contains(el, sw)) {
2701		   //console.log('w3_switch_set_value: click()...');
2702		   //console.log(el);
2703			el.click();
2704		}
2705	});
2706}
2707
2708
2709////////////////////////////////
2710// buttons: single, clickable icon
2711////////////////////////////////
2712
2713function w3int_btn_evt(ev, path, cb, cb_param)
2714{
2715   var el = w3_el(path);
2716   if (!w3_contains(el, 'w3-disabled')) {
2717      //console.log('w3int_btn_evt ev.type='+ ev.type +' path='+ path +' cb='+ cb +' cb_param='+ cb_param);
2718
2719      if (w3int.autorepeat_canceller_path != null) {
2720         var diff = (w3int.autorepeat_canceller_path != path);
2721         var same = (w3int.autorepeat_canceller_path == path && ev.type == 'mousedown');
2722         if (diff || same) {
2723            //console.log(path +' CANCELLED '+ w3int.autorepeat_canceller_path);
2724            //if (kiwi_isMobile()) alert(path +' CANCELLED '+ w3int.autorepeat_canceller_path +' diff='+ TF(diff) +' same='+ TF(same));
2725            kiwi_clearInterval(w3int.autorepeat_canceller_interval);
2726            w3int.autorepeat_canceller_path = null;
2727         }
2728      }
2729      
2730      w3_check_restart_reboot(ev.currentTarget);
2731      
2732      var hold = w3_match_wildcard(el, 'w3-hold');    // matches w3-hold and w3-hold-<delay>
2733      var hold_done = w3_contains(el, 'w3-hold-done');
2734      /*
2735      if (hold != false && cb == 'freq_memory_menu_icon_cb') {
2736         console.error('w3-hold');
2737         //console.log('path='+ path +' cb='+ cb);
2738         console.log(hold);
2739         console.log('hold_done='+ hold_done);
2740      }
2741      */
2742      //canvas_log(ev.type + ((hold != false)? ' w3H':'') + (hold_done? '+w3D':''));
2743
2744      if (hold != false) {
2745         //console.log('w3int_btn_evt HOLD '+ ev.type);
2746         if (ev.type == 'mousedown' || ev.type == 'touchstart') {
2747            el.hold_triggered = false;
2748            var delay = isNumberElse(Math.abs(hold.num), 500);
2749            //console.error('w3-hold: '+ hold.prop +' '+ hold.num +' delay='+ delay);
2750            el.hold_timeout = setTimeout(function() {
2751               //canvas_log('HSU');
2752               el.hold_triggered = true;
2753               //console.log('w3int_btn_evt HOLD_TRIGGERED, RUN ACTION');
2754               if (cb) {
2755                  //canvas_log('EvHOLD');
2756                  w3_call(cb, path, cb_param, /* first */ false, { type: 'hold' });
2757               }
2758            }, delay);
2759            return ignore(ev);   // don't run callback below
2760         } else {
2761            //canvas_log('HT='+ el.hold_timeout);
2762            if ((ev.type == 'click' || ev.type == 'mouseout' || ev.type == 'touchend') && isArg(el.hold_timeout)) {
2763               //if (ev.type == 'mouseout') console.log('MOUSEOUT-HOLD');
2764               var timeout = el.hold_timeout;
2765               var triggered = el.hold_triggered;
2766               el.hold_timeout = null;
2767               el.hold_triggered = false;
2768               kiwi_clearTimeout(timeout);
2769               if (triggered) {
2770                  //console.log('w3int_btn_evt HOLD_PREV_TRIGGERED, IGNORE CLICK');
2771                  //canvas_log('HTR');
2772                  if (hold_done) {
2773                     //canvas_log('EvDONE');
2774                     w3_call(cb, path, cb_param, /* first */ false, { type: 'hold-done' });
2775                  }
2776                  w3int_post_action();
2777                  return ignore(ev);
2778               }
2779               //canvas_log('HTO');
2780            
2781               // fall through -- normal mouseup/touchend click before hold timer expired
2782            }
2783         }
2784      }
2785   
2786      // ignore spurious mouseout events when w3-hold is enabled
2787      if (ev.type == 'mouseout') {
2788         //console.log('MOUSEOUT-REG');
2789         return;
2790      }
2791      
2792      // cb is a string because can't pass an object to onclick
2793      if (cb) {
2794         //canvas_log('CB');
2795         w3_call(cb, path, cb_param, /* first */ false, ev);   // buttons don't have first callback
2796      }
2797   }
2798
2799   w3int_post_action();
2800}
2801
2802function w3_autorepeat_canceller(path, interval)
2803{
2804   w3int.autorepeat_canceller_path = isUndefined(path)? null : path;
2805   w3int.autorepeat_canceller_interval = isUndefined(interval)? null : interval;
2806}
2807
2808function w3_cb_param_encode(cbp)
2809{
2810   if (isNoArg(cbp)) {
2811      return 0;
2812   }
2813   
2814   if (isObject(cbp)) {
2815      return encodeURIComponent(JSON.stringify(cbp));
2816   }
2817   
2818   return cbp;
2819}
2820
2821function w3_cb_param_decode(cbp)
2822{
2823   if (isNoArg(cbp)) {
2824      return 0;
2825   }
2826   
2827   if (isString(cbp)) {
2828      cbp = decodeURIComponent(cbp);
2829      var obj;
2830      if (cbp[0] == '{') {
2831         try {
2832            obj = JSON.parse(cbp);
2833         } catch(ex) {
2834            return cbp;
2835         }
2836         return obj;
2837      }
2838   }
2839   
2840   return cbp;
2841}
2842
2843function w3int_btn_event_handlers(psa, path, cb, cb_param, dump)
2844{
2845   var type;
2846   var momentary = psa.includes('w3-momentary');
2847   var hold = psa.includes('w3-hold');
2848   var custom_events = psa.includes('w3-custom-events');
2849
2850	cb_param = w3_cb_param_encode(cb_param);
2851	var ev_cb = function(cbp) { return sprintf('"w3int_btn_evt(event, %s, %s, %s)"', sq(path), sq(cb), sq(cbp)); };
2852	var onclick = cb? ('onclick='+ ev_cb(cb_param)) : '';
2853
2854	if (cb && momentary && !hold && !custom_events) {
2855	   onclick += ' onmousedown='+ ev_cb(0);
2856	   onclick += ' ontouchstart='+ ev_cb(0);
2857	   type = 'MOM';
2858	} else
2859	if (cb && hold && !momentary && !custom_events) {
2860	   onclick += ' onmousedown='+ ev_cb(cb_param);
2861	   onclick += ' onmouseout='+ ev_cb(cb_param);        // needed to cancel w3-hold
2862	   onclick += ' ontouchstart='+ ev_cb(cb_param);
2863	   onclick += ' ontouchend='+ ev_cb(cb_param);        // mobile doesn't generate a click event
2864	   type = 'HOLD';
2865	} else
2866	if (cb && (custom_events || hold) && !momentary) {
2867	   onclick += ' onmouseenter='+ ev_cb(cb_param);
2868	   onclick += ' onmouseleave='+ ev_cb(cb_param);
2869	   onclick += ' onmousedown='+ ev_cb(cb_param);
2870	   onclick += ' onmousemove="ignore(event);"';
2871	   onclick += ' onmouseup="ignore(event);"';
2872
2873	   onclick += ' ontouchstart='+ ev_cb(cb_param);
2874	   onclick += ' ontouchmove="ignore(event);"';
2875	   onclick += ' ontouchend='+ ev_cb(cb_param);        // mobile doesn't generate a click event
2876	   type = 'CUS';
2877	} else
2878	   type = 'DEF';
2879
2880   if (dump) canvas_log('w3int_btn_event_handlers '+ type);
2881	return onclick;
2882}
2883
2884function w3int_button(psa, path, text, cb, cb_param)
2885{
2886   var dump = psa.includes('w3-dump');
2887	var onclick = w3int_btn_event_handlers(psa, path, cb, cb_param, dump);
2888	
2889	// w3-round-large listed first so its '!important' can be overriden by subsequent '!important's
2890	var default_style = (psa.includes('w3-round') || psa.includes('w3-circle'))? '' : ' w3-round-6px';
2891	var noactive = psa.includes('w3-noactive')? ' class-button-noactive w3-ext-btn-noactive' : '';
2892	var right = psa.includes('w3-btn-right')? 'w3-ialign-right' : '';
2893   var psa3 = w3_psa3(psa);
2894   var psa_outer = w3_psa(psa3.left, right);
2895	var psa_inner = w3_psa(psa3.right, path +' w3-btn w3-ext-btn'+ default_style + noactive, '', onclick);
2896   if (dump) {
2897      console.log('w3_button');
2898      console.log(psa3);
2899      console.log('psa_outer='+ psa_outer);
2900      console.log('psa_inner='+ psa_inner);
2901   }
2902	var s = '<button '+ psa_inner +'>
2902'+ text +'</button>';
2903	if (psa_outer != '') s = '<div '+ psa_outer +'>'+ s +'</div>';
2904	if (psa_outer.includes('w3-dump')) console.log(s);
2905	return s;
2906}
2907
2908function w3_button(psa, text, cb, cb_param)
2909{
2910	var path = 'id-btn-grp-'+ w3int.btn_grp_uniq.toString();
2911	w3int.btn_grp_uniq++;
2912   return w3int_button(psa, path, text, cb, cb_param);
2913}
2914
2915function w3_button_path(psa, path, text, cb, cb_param)
2916{
2917   return w3int_button(psa, path, text, cb, cb_param);
2918}
2919
2920function w3_button_text(path, text, color_or_add_color, remove_color)
2921{
2922   var el = w3_el(path);
2923   if (!el) return null;
2924   if (isArg(text))
2925      el.innerHTML = text;
2926   if (color_or_add_color) {
2927      if (color_or_add_color.startsWith('w3-')) {
2928         w3_remove(el, remove_color);
2929         w3_add(el, color_or_add_color);
2930      } else
2931         el.style.color = color_or_add_color;
2932   }
2933   return el;
2934}
2935
2936
2937////////////////////////////////
2938// icon
2939////////////////////////////////
2940
2941function w3_icon(psa, fa_icon, size, color, cb, cb_param)
2942{
2943   var dump = psa.includes('w3-dump');
2944
2945   // by default use pointer cursor if there is a callback
2946	var pointer = (isNonEmptyString(cb))? ' w3-pointer':'';
2947	var path = 'id-btn-grp-'+ w3int.btn_grp_uniq.toString();
2948	w3int.btn_grp_uniq++;
2949
2950	var font_size = null;
2951	if (isNumber(size) && size >= 0) font_size = px(size);
2952	else
2953	if (isString(size)) font_size = size;
2954	font_size = font_size? (' font-size:'+ font_size +';') : '';
2955
2956   color = color || '';
2957   var c = color.split('|');
2958   color = '';
2959   if (c[0] != '') color = ' color:'+ c[0] +';';
2960   if (c.length >= 2 && c[1] != '') color += ' background-color:'+ c[1] +';';
2961
2962	var onclick = w3int_btn_event_handlers(psa, path, cb, cb_param, dump);
2963
2964	var p = w3_psa(psa, path +' w3-ext-icon'+ pointer +' fa '+ fa_icon, font_size + color, onclick);
2965	var s = '<i '+ p +'></i>';
2966	//console.log(s);
2967	return s;
2968}
2969
2970function w3_spin(el, cond)
2971{
2972   el = w3_el(el);
2973   if (!el) return null;
2974   cond = isArg(cond)? cond : true;
2975   w3_remove_then_add(el, 'fa-spin', cond? 'fa-spin':'');
2976}
2977
2978/*
2979
2980// prototype of callbacks using ev.currentTarget instead of path
2981// switch to using this someday
2982
2983function w3int_icon_click(ev, cb, cb_param)
2984{
2985   console.log('w3int_icon_click cb='+ cb +' cb_param='+ cb_param);
2986   var el = ev.currentTarget;
2987   console.log(el);
2988   w3_check_restart_reboot(el);
2989
2990   // cb is a string because can't pass an object to onclick
2991   if (cb) {
2992      w3_call(cb, el, cb_param, false);
2993   }
2994
2995   w3int_post_action();
2996}
2997
2998function w3_icon_cb2(psa, fa_icon, size, color, cb, cb_param)
2999{
3000   // by default use pointer cursor if there is a callback
3001	var pointer = (cb && cb != '')? ' w3-pointer':'';
3002	var path = 'id-btn-grp-'+ w3int.btn_grp_uniq.toString();
3003	w3int.btn_grp_uniq++;
3004	cb_param = cb_param || 0;
3005
3006	var font_size = null;
3007	if (isNumber(size) && size >= 0) font_size = px(size);
3008	else
3009	if (isString(size)) font_size = size;
3010	font_size = font_size? (' font-size:'+ font_size +';') : '';
3011
3012	color = (color && color != '')? (' color:'+ color) : '';
3013	var onclick = cb? ('onclick="w3int_icon_click(event, '+ sq(path) +', '+ sq(cb) +', '+ sq(cb_param) +')"') : '';
3014	var p = w3_psa(psa, path + pointer +' fa '+ fa_icon, font_size + color, onclick);
3015	var s = '<i '+ p +'></i>';
3016	//console.log(s);
3017	return s;
3018}
3019
3020*/
3021
3022
3023////////////////////////////////
3024// input
3025////////////////////////////////
3026
3027// Detect empty lines (only \n) because onchange event not fired in that case.
3028// Also detect and process control character sequences.
3029function w3int_input_keydown(ev, path, cb)
3030{
3031   var i, s;
3032   var k = ev.key.toUpperCase();
3033   var ctrl = ev.ctrlKey;
3034	var el = w3_el(path);
3035   if (!el) return;
3036   var input_any_key = w3_contains(el, 'w3-input-any-key');
3037   var input_any_change = w3_contains(el, 'w3-input-any-change');
3038   var disabled = w3_parent_with(el, 'w3-disabled');
3039   //w3_remove(el, 'kiwi-pw');
3040   var trace = w3_contains(el, 'w3-trace');
3041   if (trace) console.log('w3int_input_keydown CE k='+ k + (ctrl? ' CTRL ':'') +' val=<'+ el.value +'> cb='+ cb +' disabled='+ disabled);
3042   var cb_a = cb.split('|');
3043   
3044   if (disabled) {
3045      if (trace) console.log('w3int_input_keydown w3-disabled');
3046      return;
3047   }
3048   
3049   if (input_any_key && cb_a[1]) {
3050      //console.log('input_any_key');
3051      //console.log(cb_a);
3052      //console.log('w3int_input_keydown: input_any_key '+ k +' cb_a[1]='+ cb_a[1]);
3053      //event_dump(ev, "input_any_change", true);
3054      w3_call(cb_a[1], ev, true);
3055   }
3056
3057	if (ev.key == 'Enter') {
3058      if (input_any_change) {
3059         // consider unchanged input value followed by Enter, at the end of input text, to be an input change
3060         if (trace) console.log('w3int_input_keydown: w3-input-any-change + Enter');
3061
3062         // A newline is going to be added to el.value after w3int_input_keydown() returns.
3063         // If we are currently positioned somewhere in the potential run of newlines at the end of the text
3064         // then remove all the trailing newlines so only a single newline will remain.
3065         s = el.value;
3066         var textLength = el.value.length;   // no el.textLength in Safari & Chrome!
3067         if (trace) console.log('newline strip: ss='+ el.selectionStart +' tl='+ textLength);
3068         for (i = el.selectionStart; i < textLength; i++) {
3069            if (s.charAt(i) != '\n')
3070               break;
3071         }
3072         if (trace) console.log('newline strip: i='+ i +' tl='+ textLength);
3073         if (i == textLength) {
3074            while (s.endsWith('\n'))
3075               s = s.slice(0, -1);
3076            if (trace) console.log('s='+ JSON.stringify(s));
3077            w3_set_value(el, s);    // update field so el.selectionStart etc below are also updated
3078         }
3079
3080         if (trace) console.log('ss='+ el.selectionStart +' se='+ el.selectionEnd +' tl='+ textLength);
3081         if (
3082            // cursor at end of text
3083            (el.selectionStart == el.selectionEnd && el.selectionStart == textLength) ||
3084            
3085            // all text is selected
3086            (el.selectionStart == 0 && el.selectionEnd == textLength) ||
3087            
3088            // cause empty input lines followed by Enter to send empty command to shell
3089            (el.value == '')
3090            
3091            ) {
3092            if (trace) {
3093               console.log('w3_input_change '+ path +' ...');
3094               console.log('el.value='+ JSON.stringify(el.value));
3095               console.log(el);
3096            }
3097            w3_input_change(path, cb, 'kd1', ev);
3098            if (trace) {
3099               console.log('w3_input_change '+ path +'(2)...');
3100               console.log('el.value='+ JSON.stringify(el.value));
3101               console.log(el);
3102            }
3103            //w3_dismiss_keyboard(el);
3104         }
3105      } else
3106	   if (el.value == '') {
3107         // cause empty input lines followed by Enter to send empty command to shell
3108         if (trace) console.log('w3int_input_keydown: empty line + Enter');
3109         w3_input_change(path, cb, 'kd2', ev);
3110         w3_dismiss_keyboard(el);
3111      }
3112	}
3113
3114   // if Delete key (Backspace) when entire value is selected then consider it an input change
3115	if (ev.key == 'Backspace' && input_any_change && el.selectionStart == 0 && el.selectionEnd == el.value.length) {
3116      if (trace) console.log('w3int_input_keydown Delete: len='+ el.value.length +' ss='+ el.selectionStart +' se='+ el.selectionEnd);
3117      el.value = '';
3118      w3_input_change(path, cb, 'kd3', ev);
3119      w3_dismiss_keyboard(el);
3120	}
3121
3122	// prevent dom parents from seeing bubbling event
3123	// Don't call cancelEvent() because that does a stopImmediatePropagation() which prevents the
3124	// subsequent keyup & change/input events from occurring.
3125	ev.stopPropagation();
3126	
3127   if (trace) {
3128      console.log('w3_input_change '+ path +'(3)...');
3129      console.log('el.value='+ JSON.stringify(el.value));
3130      console.log(el);
3131   }
3132}
3133
3134function w3int_input_keyup(ev, path, cb)
3135{
3136   var k = ev.key.toUpperCase();
3137   var ctrl = ev.ctrlKey;
3138	var el = w3_el(path);
3139   if (!el) return;
3140
3141   /*
3142      if (ev.key == 'Backspace' && el.value == '')
3143         w3_input_change(path, cb, 'ku', ev);
3144   */
3145
3146   var disabled = w3_parent_with(el, 'w3-disabled');
3147   var trace = w3_contains(el, 'w3-trace');
3148   if (trace) console.log('w3int_input_keyup CE k='+ k + (ctrl? ' CTRL ':'') +' path='+ path +' val=<'+ el.value +'> cb='+ cb +' disabled='+ disabled);
3149	
3150   var input_any_key = w3_contains(el, 'w3-input-any-key');
3151   var cb_a = cb.split('|');
3152   if (input_any_key && cb_a[1]) {
3153      //console.log('input_any_key');
3154      //console.log(cb_a);
3155      //console.log('w3int_input_keyup: input_any_key '+ k +' cb_a[1]='+ cb_a[1]);
3156      //event_dump(ev, "input_any_change", true);
3157      w3_call(cb_a[1], ev, true);
3158   }
3159
3160	// prevent dom parents from seeing bubbling event
3161	// Don't call cancelEvent() because that does a stopImmediatePropagation() which prevents the
3162	// subsequent keyup & change/input events from occurring.
3163	ev.stopPropagation();
3164}
3165
3166// event generated from w3_input_force()
3167function w3int_input_process(ev)
3168{
3169   var a = ev.detail.split('|');
3170   var path = a[0];
3171   var cb = a[1];
3172   //console.log('w3int_input_process', {ev, a, path, cb});
3173	var el = w3_el(path);
3174	if (el) {
3175      var trace = w3_contains(el, 'w3-trace');
3176      if (trace) console.log('w3int_input_process path='+ path +' val='+ el.value +' cb='+ cb);
3177      w3int_input_change(ev, path, cb, 'w3int_input_process');
3178   }
3179}
3180
3181function w3_input_change(path, cb, from, ev)
3182{
3183   w3int_input_change(ev, path, cb, from);
3184}
3185
3186// 'from' arg (that is appended to w3_call() cb_a[] arg) is:
3187//    'ev' if called from normal onchange event
3188//    'kd' or 'ku' if 'w3-input-any-change' is specified and the "any change" criteria is met
3189//    'in' if 'w3-input-evt' is specified and an input event has been generated
3190function w3int_input_change(ev, path, cb, from)
3191{
3192   var val = '???';
3193   var retain = false;
3194   var trace = false;
3195	var el = w3_el(path);
3196	if (el) {
3197      trace = w3_contains(el, 'w3-trace');
3198      if (trace) console.log('w3int_input_change START path='+ path +' from='+ from);
3199      
3200      // cb is a string because can't pass an object to onclick
3201      if (cb) {
3202         var cb_a = cb.split('|');
3203         if (isArg(from)) cb_a.push(from);
3204         //el.select();
3205         if (trace) console.log(cb_a);
3206         if (trace) console.log('w3int_input_change CALL '+ cb_a[0] +'('+ sq(path) +', '+ sq(el.value) +', false, cb_a[])');
3207         retain = w3_call(cb_a[0], path, el.value, /* first */ false, cb_a, ev);
3208         if (from == 'in') return;
3209      }
3210
3211      w3_check_restart_reboot(el);
3212      //w3_check_cfg_reload(el);
3213      if (trace) kiwi_trace();
3214      w3_schedule_highlight(el);
3215      val = el.value;
3216
3217/*
3218      if (el.value != '' && w3_contains(el, 'w3-encrypted')) {
3219         w3_add(el, 'kiwi-pw');
3220         el.value = '(encrypted)';
3221      }
3222*/
3223   }
3224	
3225   w3_dismiss_keyboard(el);
3226   if (trace) console.log('w3int_input_change END path='+ path +' val='+ val +' retain='+ retain +' ev='+ ev);
3227   if (retain === true) {
3228      w3_field_select(el, {mobile:1});    // retain field selection
3229   } else {
3230      w3int_post_action();
3231   }
3232
3233	// prevent dom parents from seeing bubbling event
3234	// Don't call cancelEvent() because that does a stopImmediatePropagation() which prevents the
3235	// subsequent keyup & change/input events from occurring.
3236	if (isArg(ev)) ev.stopPropagation();
3237}
3238
3239function w3int_input_up_down_cb(path, cb_param, first, ev)
3240{
3241   //event_dump(ev, path, 1);
3242   if (ev.type != 'click' && ev.type != 'touch') return;
3243   var cb_a = cb_param.split('|');
3244   var delta = +cb_a[0];
3245   var id_s = cb_a[1];
3246   var cb_s = cb_a[2];
3247   var val = +w3_el(id_s).value + delta;
3248   //console.log('w3int_input_up_down_cb', {cb_param, ev});
3249   w3_input_force(id_s, cb_s, val.toFixed(0));
3250}
3251
3252// No cb_param here because w3int_input_change() passes the input value as the callback parameter.
3253// But can specify a composite callback name (i.e. "cb|param0|param1") that is passed to callback routine.
3254//
3255// NB: using w3_esc_dq(val) below eliminates the need to call admin_set_decoded_value() via
3256// admin tab *_focus() routines solely to work around the escaping of double quotes in the
3257// val issue.
3258
3259function w3_input(psa, label, path, val, cb, placeholder)
3260{
3261   var dump = psa.includes('w3-dump');
3262   if (dump) console.log('w3_input()...');
3263	var id = w3_add_id(path);
3264	cb = cb || '';
3265	var phold = placeholder? ('placeholder="'+ placeholder +'"') : '';
3266	var custom = psa.includes('w3-custom-events');
3267	var onchange = ' onchange="w3int_input_change(event, '+ sq(path) +', '+ sq(cb) +', '+ sq('ev') +')"';
3268	var oninput = psa.includes('w3-input-evt')? (' oninput="w3int_input_change(event, '+ sq(path) +', '+ sq(cb) +', '+ sq('in') +')"') : '';
3269	var onkeydown = ' onkeydown="w3int_input_keydown(event, '+ sq(path) +', '+ sq(cb) +')"';
3270	var onkeyup = ' onkeyup="w3int_input_keyup(event, '+ sq(path) +', '+ sq(cb) +')"';
3271	var events = (path && !custom)? (onchange + oninput + onkeydown + onkeyup) : '';
3272	//var auto = psa.includes('w3-no-auto-anything')? ' autocomplete="off" autocorrect="off" autocapitalize="off"' : '';
3273	var auto = ' autocomplete="off" autocorrect="off" autocapitalize="off"';
3274	val = ' value='+ dq(w3_esc_dq(val) || '');
3275	var inline = psa.includes('w3-label-inline');
3276	var bold = !psa.includes('w3-label-not-bold');
3277	
3278	var label_right = '';
3279	if (isArray(label)) {
3280	   var a = label;
3281	   label = a[0] || '';
3282	   label_right = a[1] || '';
3283	}
3284	label = label || '';
3285	var label_spacing = (label != '' && inline)? 'w3int-margin-input' : '';
3286
3287	// type="password" is no good because it forces the submit to be https which we don't support
3288	//var type = 'type='+ (psa.includes('w3-password')? '"password"' : '"text"');
3289	
3290   var psa3 = w3_psa3(psa);
3291   if (dump) {
3292      console.log(psa);
3293      console.log(psa3);
3294   }
3295   var psa_outer = w3_psa(psa3.left, inline? 'w3-inline-flex':'');
3296   var psa_label = w3_psa_mix(psa3.middle, (label != '' && bold)? 'w3-bold':'');
3297   var style = psa.includes('w3-no-styling')? '' : 'w3-input w3-border w3-hover-shadow';
3298	var type = psa3.right.includes('type=')? '' : 'type="text"';
3299	var psa_inner = w3_psa(psa3.right, w3_sb(id, style, label_spacing), '', w3_sb(type, phold));
3300	if (dump) console.log('O:['+ psa_outer +'] L:['+ psa_label +'] I:['+ psa_inner +']');
3301	
3302   // NB: include id in an id= for benefit of keyboard shortcut field detection
3303	var in_s = '<input id='+ dq(id) +' '+ psa_inner + val + events + auto +'>';
3304	var cb_s = '|'+ id +'|'+ cb;
3305	
3306	if (psa.includes('w3-up-down')) {
3307	   in_s = w3_inline('',
3308	      w3_div('w3-flex-col w3-margin-R-4',
3309	         w3_icon('w3-momentary w3-pointer w3-margin-B-1||title="up"', 'fa-arrow-up', 15, 'orange', 'w3int_input_up_down_cb', '1'+ cb_s),
3310	         w3_icon('w3-momentary w3-pointer w3-margin-T-1||title="down"', 'fa-arrow-down', 15, 'cyan', 'w3int_input_up_down_cb', '-1'+ cb_s)
3311	      ),
3312	      in_s
3313	   );
3314   }
3315
3316	var s =
3317	   '<div '+ psa_outer +'>' +
3318         w3int_label(psa_label, label, path) +
3319         in_s +
3320         label_right +
3321      '</div>';
3322	if (dump) console.log(s);
3323	//w3int_input_set_id(id);
3324	return s;
3325}
3326
3327function w3_input_force(path, cb, input)
3328{
3329   //console.log('w3_input_force', {path, cb, input});
3330   var el = w3_el(path);
3331   if (!el) return;
3332   el.addEventListener('input', w3int_input_process, w3.CAPTURING);
3333   el.value = input.replace('&vbar;', '|');
3334   el.dispatchEvent(new CustomEvent('input', { detail: path +'|'+ cb }));
3335   el.removeEventListener('input', w3int_input_process, w3.CAPTURING);
3336}
3337
3338/*
3339function w3int_input_set_id(id)
3340{
3341   console.log('### w3int_input_set_id el='+ id +' DO NOT USE');
3342   return;
3343	//if (id == '') return;
3344	//setTimeout(function() { w3int_input_set_id_timeout(id); }, 3000);
3345}
3346
3347function w3int_input_set_id_timeout(id)
3348{
3349   var el = w3_el(id);
3350   //console.log('### w3int_input_set_id_timeout el='+ id +' CONSIDER');
3351   if (!el) return;
3352   if (el.id != '') {
3353      console.log('### w3int_input_set_id_timeout el='+ id +' id='+ id +' ALREADY SET?');
3354      return;
3355   }
3356   w3_iterate_classList(el, function(className, idx) {
3357	   if (className.startsWith('id-')) {
3358         console.log('### w3int_input_set_id_timeout el='+ id +' id='+ id +' SET --------');
3359	      el.id = className;
3360	   }
3361   });
3362}
3363*/
3364
3365// used when current value should come from config param
3366function w3_input_get(psa, label, path, cb, init_val, placeholder)
3367{
3368   // FIXME
3369   //var update_path_var = psa.includes('w3-update');
3370   var save = psa.includes('w3-defer')? EXT_SAVE_DEFER : undefined;
3371	var cur_val = ext_get_cfg_param(path, (init_val == undefined)? null : init_val, save);
3372	
3373	// NB: This is still needed because legacy params might still have URI encoding (i.e. %NN)
3374	// whereas our newer model that stores json does not. But kiwi_decodeURIComponent() will
3375	// correct e.g. any singlet '%' saved in a json param.
3376	cur_val = kiwi_decodeURIComponent('w3_input_get:'+ path, cur_val);
3377	//console.log('w3_input_get: path='+ path +' cur_val="'+ cur_val +'" placeholder="'+ placeholder +'"');
3378	return w3_input(psa, label, path, cur_val, cb, placeholder);
3379}
3380
3381
3382////////////////////////////////
3383// textarea
3384////////////////////////////////
3385
3386// The test for 'w3-input-any-change' that suppresses inclusion of the 'onchange=' event handler
3387// fixes a strange problem we noticed: With the 'onchange=' the actual onchange event is long-delayed
3388// until the first unrelated click completely outside the textarea! This ends up causing double calls
3389// to w3int_input_change(). The first from w3int_input_keydown() due to w3-input-any-change and the second
3390// from the delayed change event.
3391//
3392// The problem doesn't seem to occur when w3-input-any-change is used with w3_input()
3393
3394function w3_textarea(psa, label, path, val, rows, cols, cb)
3395{
3396	var id = w3_add_id(path);
3397	var spacing = (label != '')? ' w3-margin-T-8' : '';
3398	var custom = psa.includes('w3-custom-events');
3399	var onchange = psa.includes('w3-input-any-change')? '' : (' onchange="w3int_input_change(event, '+ sq(path) +', '+ sq(cb) +')"');
3400	var onkeydown = ' onkeydown="w3int_input_keydown(event, '+ sq(path) +', '+ sq(cb) +')"';
3401	var onkeyup = ' onkeyup="w3int_input_keyup(event, '+ sq(path) +', '+ sq(cb) +')"';
3402	var events = (path && !custom)? (onchange + onkeydown + onkeyup) : '';
3403	var bold = !psa.includes('w3-label-not-bold');
3404   var psa3 = w3_psa3(psa);
3405   var psa_label = w3_psa_mix(psa3.middle, (label != '' && bold)? 'w3-bold':'');
3406	var psa_inner = w3_psa(psa3.right, 'w3-input w3-border w3-hover-shadow '+ id + spacing, '', 'rows='+ dq(rows) +' c
3406ols='+ dq(cols));
3407	val = val || '';
3408
3409	var s =
3410	   w3_div(psa3.left,
3411         w3int_label(psa_label, label, path) +
3412		   '<textarea '+ psa_inner + events +'>'+ val +'</textarea>'
3413		);
3414	//if (psa.includes('w3-dump')) console.log('$');
3415	//if (psa.includes('w3-dump')) console.log(psa3);
3416	//if (psa.includes('w3-dump')) console.log(psa_inner);
3417	//if (psa.includes('w3-dump')) console.log('label='+ label);
3418	//if (psa.includes('w3-dump')) console.log('val='+ val);
3419	if (psa.includes('w3-dump')) console.log('s='+ s);
3420	return s;
3421}
3422
3423// used when current value should come from config param
3424function w3_textarea_get_param(psa, label, path, rows, cols, cb, init_val)
3425{
3426   // FIXME
3427   //var update_path_var = psa.includes('w3-update');
3428   var save = psa.includes('w3-defer')? EXT_SAVE_DEFER : undefined;
3429	var cur_val = ext_get_cfg_param(path, (init_val == undefined)? null : init_val, save);
3430	
3431	// NB: This is still needed because legacy params might still have URI encoding (i.e. %NN)
3432	// whereas our newer model that stores json does not. But kiwi_decodeURIComponent() will
3433	// correct e.g. any singlet '%' saved in a json param.
3434	cur_val = kiwi_decodeURIComponent('w3_textarea_get_param:'+ path, cur_val);
3435	//if (psa.includes('w3-dump')) console.log('w3_textarea_get_param: path='+ path +' cur_val="'+ cur_val +'"');
3436	return w3_textarea(psa, label, path, cur_val, rows, cols, cb);
3437}
3438
3439
3440////////////////////////////////
3441// alert
3442////////////////////////////////
3443
3444function w3_alert(id, psa, msg, close, opts) {
3445   path = 'id-alert-'+ id +'-'+ w3int.alert_seq++;
3446   opts = opts || {};
3447   if (!opts.no_cancel) w3_alert_cancel();
3448   var width = opts.width || '650px';
3449   var top = opts.top || '50%';
3450   var left = opts.left || '50%';
3451   // translate[XY](-50%) so e.g. top:50% left:50% are referenced to div center
3452   var css = sprintf(
3453      'position: fixed; top: %s; left: %s; transform: translateX(-50%%) translateY(-50%%); width: %s; ' +
3454      'background: #444; color: white; padding: 15px 25px; border-radius: 8px; ' +
3455      'box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 9999; font-family: sans-serif;',
3456      top, left, width);
3457   var psa3 = w3_psa3(psa);
3458   var psa_outer = w3_psa(psa3.middle, path, css);
3459   var psa_inner = w3_psa(psa3.right);
3460   //console.info(psa_outer);
3461   //console.info(psa_inner);
3462   var html =
3463      w3_div(psa_outer,
3464         w3_text('w3-wrap '+ psa_inner, msg),
3465         opts.fadeout? '' : w3_button('w3-margin-T-16 w3-green', 'Close', 'w3int_alert_ok', path)
3466      );
3467   var alert = w3_create_appendElement('id-kiwi-body', 'div', html, path);
3468   w3int.alert = { path:path, active:true, close:close };
3469   if (opts.fadeout) {
3470      setTimeout(function() { w3_alert_close(); }, opts.fadeout);
3471   }
3472}
3473
3474function w3int_alert_ok(path, cb_param) {
3475   //console.log('w3int_alert_ok: path_id='+ w3int.alert.path +' path='+ path +' cb_param='+ cb_param);
3476   if (!w3_alert_active()) return;
3477   var alert = w3_el(cb_param);
3478   alert.style.transition = 'opacity 0.5s';
3479   alert.style.opacity = '0';
3480   setTimeout(function() { alert.remove(); }, 500);
3481   var close = w3int.alert.close;
3482   w3int.alert = {};    // in case close routine does another alert
3483   w3_call(close);
3484}
3485
3486function w3_alert_active() { return (w3int.alert.active == true); }
3487
3488function w3_alert_cancel() {
3489   if (w3_alert_active()) {
3490      w3int.alert.close = null;
3491      w3int_alert_ok(null, w3int.alert.path);
3492   }
3493}
3494
3495function w3_alert_close() {
3496   if (w3_alert_active()) {
3497      w3int_alert_ok(null, w3int.alert.path);
3498   }
3499}
3500
3501
3502////////////////////////////////
3503// checkbox
3504////////////////////////////////
3505
3506function w3int_checkbox_change(path, cb, cb_param)
3507{
3508	var el = w3_el(path);
3509	w3_check_restart_reboot(el);
3510	
3511	// cb is a string because can't pass an object to onclick
3512	if (cb) {
3513	   //console.log('w3int_checkbox_change el='+ el +' checked='+ el.checked);
3514		//el.select();
3515		w3_schedule_highlight(el);
3516		w3_call(cb, path, el.checked, /* first */ false, cb_param);
3517	}
3518
3519   w3int_post_action();
3520}
3521
3522function w3_checkbox(psa, label, path, checked, cb, cb_param)
3523{
3524   var dump = psa.includes('w3-dump');
3525	var id = w3_add_id(path);
3526	var onchange = ' onchange="w3int_checkbox_change('+ sq(path) +', '+ sq(cb) +', '+ sq(cb_param) +')"';
3527	var checked_s = checked? ' checked' : '';
3528	var inline = psa.includes('w3-label-inline');
3529	var left = psa.includes('w3-label-left');
3530	var bold = !psa.includes('w3-label-not-bold');
3531	var spacing = (label != '' && inline)? (left? ' w3-margin-L-8' : ' w3-margin-R-8') : '';
3532
3533   var psa3 = w3_psa3(psa);
3534   var psa_outer = w3_psa(psa3.left, inline? 'w3-inline-flex':'');
3535   var psa_label = w3_psa_mix(psa3.middle, (label != '' && bold)? 'w3-bold':'');
3536	var psa_inner = w3_psa(psa3.right, 'w3-input w3-width-auto w3-border w3-pointer w3-hover-shadow '+ id + spacing, '', 'type="checkbox"');
3537
3538   var ls = w3int_label(psa_label, label, path);
3539   var cs = '<input '+ psa_inner + checked_s + onchange +'>';
3540	var s =
3541	   '<div '+ psa_outer +'>' +
3542	      (left? (ls + cs) : (cs + ls)) +
3543      '</div>';
3544
3545	// run the callback after instantiation with the initial control value
3546	if (cb)
3547		setTimeout(function() {
3548			//console.log('w3_checkbox: initial callback: '+ cb +'('+ sq(path) +', '+ checked +')');
3549			w3_call(cb, path, checked, /* first */ true, cb_param);
3550		}, 500);
3551
3552	if (dump) console.log(s);
3553	return s;
3554}
3555
3556// used when current value should come from config param
3557function w3_checkbox_get_param(psa, label, path, cb, init_val, cb_param)
3558{
3559   var update_path_var = psa.includes('w3-update');
3560   var save = psa.includes('w3-defer')? EXT_SAVE_DEFER : undefined;
3561	var cur_val = ext_get_cfg_param(path, (init_val == undefined)? null : init_val, save, update_path_var);
3562	//console.log('w3_checkbox_get_param: path='+ path +' update_path_var='+ update_path_var +' cur_val='+ cur_val +' init_val='+ init_val);
3563	return w3_checkbox(psa, label, path, cur_val, cb, cb_param);
3564}
3565
3566function w3_checkbox_get(path)
3567{
3568   var el = w3_el(path);
3569   if (!el) return;
3570	return (el.checked? true:false);
3571}
3572
3573function w3_checkbox_set(path, checked)
3574{
3575   var el = w3_el(path);
3576   //console.log('w3_checkbox_set path='+ path +' checked='+ checked +' el=...');
3577   //console.log(el);
3578   if (!el) return;
3579	el.checked = checked? true:false;
3580}
3581
3582
3583////////////////////////////////
3584// select (HTML menu)
3585////////////////////////////////
3586
3587var W3_SELECT_SHOW_TITLE = -1;
3588
3589// Event behavior of w3_select() i.e. HTML <select>|<option> elements
3590//    menu open: MD focus
3591//    change menu item: MU blur change click
3592//    same menu item: MD MU w3_click-tgt=OPTION-BLUR blur FSEL click
3593//    when menu popup doesn't cover cursor: MU w3_click-tgt=SELECT blur FSEL click
3594//
3595// Since change+blur events are not fired if the same menu item is clicked
3596// must do blur here on click event. But only if same selected item is detected
3597// by ev.target being an <option> element.
3598
3599function w3int_select_click(ev, path, cb, cb_param)
3600{
3601   // ev.target = SELECT|OPTION, ev.currentTarget = SELECT
3602   //console.log('w3int_select_click: '+ w3_id(ev.currentTarget));
3603	var el = ev.target;
3604	if (el && el.nodeName && el.nodeName == 'OPTION') {
3605      //console.log('w3int_select_click: same menu item clicked: BLUR and FSEL');
3606	   el.blur();
3607      w3int_post_action();
3608   }
3609}
3610
3611function w3int_select_change(ev, path, cb, cb_param)
3612{
3613   // ev.target = SELECT|OPTION, ev.currentTarget = SELECT
3614   //console.log('w3int_select_change: CHANGE '+ w3_id(ev.currentTarget));
3615	var el = ev.currentTarget;
3616	w3_check_restart_reboot(el);
3617
3618	// cb is a string because can't pass an object to onclick
3619	if (cb) {
3620		w3_call(cb, path, el.value, /* first */ false, cb_param);
3621	}
3622	
3623   w3int_post_action();
3624}
3625
3626function w3int_select(psa, label, title, path, sel, opts_s, cb, cb_param)
3627{
3628	var id = w3_add_id(path);
3629	var first = '';
3630
3631	if (title != '') {
3632      var as = title.split('<br>');
3633      as.forEach(function(s,i) {
3634         first += '<option value="-1" '+ ((sel == W3_SELECT_SHOW_TITLE && i == 0)? 'selected':'') +' disabled>' + s +'</option>';
3635      });
3636	} else {
3637		if (sel == W3_SELECT_SHOW_TITLE) sel = 0;
3638	}
3639	
3640	var inline = psa.includes('w3-label-inline');
3641	var bold = !psa.includes('w3-label-not-bold');
3642	var spacing = (label != '' && !inline && !psa.includes('w3-margin-T-'))? 'w3-margin-T-8' : '';
3643	if (inline) spacing += 'w3-margin-left';
3644	if (cb == undefined) cb = '';
3645	var onchange = 'onchange="w3int_select_change(event, '+ sq(path) +', '+ sq(cb) +', '+ sq(cb_param) +')"';
3646	var onclick = 'onclick="w3int_select_click(event, '+ sq(path) +', '+ sq(cb) +', '+ sq(cb_param) +')"';
3647
3648   var psa3 = w3_psa3(psa);
3649   var psa_outer = w3_psa(psa3.left, inline? 'w3-inline-flex':'');
3650   var psa_label = w3_psa_mix(psa3.middle, (label != '' && bold)? 'w3-bold':'');
3651	var psa_inner = w3_psa(psa3.right, w3_sb(id, 'w3-select-menu', spacing), '', onchange +' '+ onclick);
3652
3653	var s =
3654	   '<div '+ psa_outer +'>' +
3655         w3int_label(psa_label, label, path) +
3656         // need <br> because both <label> and <select> are inline elements
3657         ((!inline && label != '')? '<br>':'') +
3658         '<select '+ psa_inner +'>'+ first + opts_s +'</select>' +
3659      '</div>';
3660
3661	// run the callback after instantiation with the initial control value
3662	if (cb && sel != W3_SELECT_SHOW_TITLE)
3663		setTimeout(function() {
3664			//console.log('w3_select: initial callback: '+ cb +'('+ sq(path) +', '+ sel +')');
3665			w3_call(cb, path, sel, /* first */ true, cb_param);
3666		}, 500);
3667
3668	if (psa.includes('w3-dump')) console.log(s);
3669	return s;
3670}
3671
3672function w3int_select_options(sel, opts, show_empty)
3673{
3674   var s = '';
3675   
3676   // 'min:max'
3677   // range of integers (increment one assumed)
3678   if (isString(opts)) {
3679      var rng = opts.split(':');
3680      if (rng.length == 2) {
3681         var min = +rng[0];
3682         var max = +rng[1];
3683         var idx = 0;
3684         for (var i = min; i <= max; i++) {
3685            s += '<option value='+ dq(idx) +' '+ ((idx == sel)? 'selected':'') +'>'+ i +'</option>';
3686            idx++;
3687         }
3688      }
3689   } else
3690
3691   // [ n, n ... ]
3692   // [ 's', 's', n, 's' ... ]
3693   // [ 's', n, { first_key:text, [name:text,] key:x ... }, n ... ]
3694   // mixed array of strings, numbers or objects
3695   // if object, take first object element's value as menu option text
3696   //    *unless* value obj.name exists, then obj.name is menu option text
3697   if (isArray(opts)) {
3698      opts.forEach(function(obj, i) {
3699         if (obj == null) return;
3700         if (isObject(obj)) {
3701            //var keys = Object.keys(obj);
3702            //obj = obj[keys[0]];
3703            var hasName = isDefined(obj.name);
3704            if (hasName && !show_empty && obj.name == '') return;
3705            obj = hasName? obj.name : w3_obj_seq_el(obj, 0);
3706         }
3707         s += '<option value='+ dq(i) +' '+ ((i == sel)? 'selected':'') +'>'+ obj +'</option>';
3708      });
3709   } else
3710
3711   // { key0:text0, key1:text1, obj_text:{ [name:text] [value:i] } ... }
3712   // object: enumerate sequentially like an array.
3713   // object elements
3714   //    non-object: (number, string, etc.)
3715   //       use element value as menu option text
3716   //       option value is element's sequential position
3717   //    object:
3718   //       key itself is menu option text
3719   //          *unless* value obj.name exists, then obj.name is menu option text
3720   //       option value is elements's sequential position
3721   //          *unless* value obj.value exists, then obj.value is option value
3722   if (isObject(opts)) {
3723      if (opts == null) return '';
3724      w3_obj_enum(opts, function(key, i, obj) {
3725         var value, text, disabled = false;
3726         if (isObject(obj)) {
3727            value = isDefined(obj.value)? obj.value : i;
3728            var hasName = isDefined(obj.name);
3729            if (hasName && !show_empty && obj.name == '') return;
3730            text = hasName? obj.name : key;
3731            if (isDefined(obj.disabled) && obj.disabled) disabled = true;
3732         } else {
3733            value = i;
3734            text = obj;
3735         }
3736         s += '<option value='+ dq(value) + ((i == sel)? ' selected':'') + (disabled? ' disabled':'') +'>'+ text +'</option>\n';
3737      });
3738   }
3739   
3740   // If sel is null, e.g. as a result of a failing match from w3_obj_key_seq(),
3741   // set menu to a disabled entry named "UNKNOWN".
3742   if (sel == null) {
3743      s += '<option value=-1 selected disabled>(unknown)</option>\n';
3744   }
3745   
3746   return s;
3747}
3748
3749function w3_select(psa, label, title, path, sel, opts, cb, cb_param)
3750{
3751   var s = w3int_select_options(sel, opts, psa.includes('w3-show-empty'));
3752	if (psa.includes('w3-dump')) console.log(s);
3753   return w3int_select(psa, label, title, path, sel, s, cb, cb_param);
3754}
3755
3756// hierarchical -- menu entries interspersed with disabled (non-selectable) headers
3757// { "key0":[fv0, fv1 ...], "key1":[fv0, fv1 ...] ... }
3758// object: enumerate sequentially like an array using:
3759//    object keys as the disabled menu entries (string only, numeric or empty-string keys ignored)
3760//       '_' in key => menu header line break
3761//    arrays as the menu options
3762//       array elements are w3_first_value()'s e.g. int, string, first array value etc.
3763function w3int_select_hier(psa, label, title, path, sel, collapse, opts, cb, cb_param)
3764{
3765   var s = '';
3766   var idx = 0;
3767   if (!isObject(opts)) return;
3768
3769   w3_obj_enum(opts, function(key, i, a) {
3770      if (key != '' && !isNumber(+key)) {
3771         as = key.split('_');       // '_' => menu header line break
3772         as.forEach(function(e) {
3773            if (!e || e != '')
3774               s += '<option value='+ dq(idx++) +' disabled>'+ e +'</option> ';
3775         });
3776      }
3777      if (!isArray(a)) return;
3778
3779      a.forEach(function(el, j) {
3780         if (collapse && j >= collapse) return;     // only show first collapse number of entries
3781         var v = w3_first_value(el);
3782         if (v == 'null') return;
3783         var v2 = w3_second_value(el);
3784         var _class = v2? (' class='+ dq(v2)) : '';
3785         s += '<option value='+ dq(idx++) +' id="id-'+ i +'-'+ j +'"' + _class +'>'+ v.toString() +'</option> ';
3786      });
3787   });
3788   
3789   /*
3790   var keys = Object.keys(opts);
3791   for (var i=0; i < keys.length; i++) {
3792      var key = keys[i];
3793      s += '<option value='+ dq(idx++) +' disabled>'+ key +'</option> ';
3794      var a = opts[key];
3795      if (!isArray(a)) continue;
3796
3797      for (var j=0; j < a.length; j++) {
3798         var v = w3_first_value(a[j]);
3799         s += '<option value='+ dq(idx++) +' id="id-'+ i +'-'+ j +'">'+ v.toString() +'</option> ';
3800      }
3801   }
3802   */
3803   
3804   return w3int_select(psa, label, title, path, sel, s, cb, cb_param);
3805}
3806
3807function w3_select_hier(psa, label, title, path, sel, opts, cb, cb_param)
3808{
3809   return w3int_select_hier(psa, label, title, path, sel, 0, opts, cb, cb_param);
3810}
3811
3812function w3_select_hier_collapse(psa, label, title, path, sel, collapse, opts, cb, cb_param)
3813{
3814   return w3int_select_hier(psa, label, title, path, sel, collapse, opts, cb, cb_param);
3815}
3816
3817// conditional -- individual menu entries can be enabled/disabled
3818// [ ['s', 1|0], ... ]
3819function w3_select_conditional(psa, label, title, path, sel, opts, cb, cb_param)
3820{
3821   var s = '';
3822   var idx = 0;
3823   if (!isArray(opts)) return;
3824
3825   opts.forEach(function(el) {
3826      if (isArray(el))
3827         s += '<option value='+ dq(idx) +' '+ ((idx == sel)? 'selected':'') +' '+ (el[1]? '':'disabled') +'>'+ el[0] +'</option> ';
3828         idx++;
3829   });
3830   
3831   return w3int_select(psa, label, title, path, sel, s, cb, cb_param);
3832}
3833
3834function w3_select_set_disabled(path, value, disabled, title)
3835{
3836   w3_iterate_children(path, function(el, i) {
3837      //console.log('w3_select_set_disabled['+i+']');
3838      //console.log(el);
3839      if (el.value == value) {
3840         el.disabled = disabled? true:false;
3841         if (isArg(title)) el.title = title;
3842      }
3843   });
3844}
3845
3846// used when current value should come from config param
3847function w3_select_get_param(psa, label, title, path, opts, cb, init_val, cb_param)
3848{
3849   var update_path_var = psa.includes('w3-update');
3850   var save = psa.includes('w3-defer')? EXT_SAVE_DEFER : undefined;
3851	var cur_val = ext_get_cfg_param(path, (init_val == undefined)? 0 : init_val, save, update_path_var);
3852	return w3_select(psa, label, title, path, cur_val, opts, cb, cb_param);
3853}
3854
3855function w3_select_enum(path, func)
3856{
3857   path = w3_el(path);
3858   if (!path) return;
3859	w3_iterate_children(path, func);
3860}
3861
3862function w3_select_match(path, s)
3863{
3864   var found = false, idx = -1;
3865   w3_select_enum(path,
3866      function(e) {
3867         if (!found && !e.disabled && e.innerHTML.toLowerCase().startsWith(s)) {
3868            idx = e.value;
3869            found = true;
3870         }
3871      }
3872   );
3873   return { found:found, idx:idx };
3874}
3875
3876function w3_select_get_value(path, idx)
3877{
3878   var found = null, last_disabled = null;
3879   w3_select_enum(path,
3880      function(e) {
3881         if (e.value == idx)
3882            found = e.innerHTML;
3883         if (!found && e.disabled)
3884            last_disabled = e.innerHTML;
3885      }
3886   );
3887   return { option:found, last_disabled:last_disabled };
3888}
3889
3890function w3_select_value(path, idx, opt)
3891{
3892   if (w3_opt(opt, 'all')) {
3893      //console.log('{all:1}');
3894      w3_els(path, function(el) {
3895         //console.log(el);
3896         el.value = idx;
3897      });
3898   } else {
3899      var el = w3_el(path);
3900      if (!el) return;
3901      el.value = idx;
3902   }
3903}
3904
3905function w3_select_set_if_includes(path, s, opt)
3906{
3907   var found = false;
3908   var re = RegExp(s);
3909   w3_select_enum(path, function(option) {
3910      //console.log('w3_select_set_if_includes s=['+ s +'] consider=['+ option.innerHTML +']');
3911      if (re.test(option.innerHTML)) {
3912         if (!found) {
3913            w3_select_value(path, option.value);
3914            if (w3_opt(opt, 'cb')) {
3915               w3_call(opt.cb, path, option.value);
3916            }
3917            //console.log('w3_select_set_if_includes TRUE value='+ option.value);
3918            found = true;
3919         }
3920      }
3921   });
3922   return found;
3923}
3924
3925
3926////////////////////////////////
3927// slider
3928////////////////////////////////
3929
3930function w3int_slider_change(ev, complete, path, cb, cb_param)
3931{
3932	var el = ev.currentTarget;
3933	w3_check_restart_reboot(el);
3934	
3935	// cb is a string because can't pass an object to onclick
3936	if (cb) {
3937	   //console.log('w3int_slider_change path='+ path +' el.value='+ el.value);
3938		w3_call(cb, path, el.value, complete, /* first */ false, cb_param);
3939	}
3940	
3941	if (complete) {
3942	   //console.log('w3int_slider_change COMPLETE path='+ path +' el.value='+ el.value);
3943	   w3int_post_action();
3944	}
3945}
3946
3947function w3int_slider_wheel(evt, path, cb, cb_param, need_shift_s)
3948{
3949   var need_shift = +need_shift_s;
3950   w3int.rate_limit_evt[cb] = evt;
3951   w3int.rate_limit_need_shift[cb] = need_shift;
3952   w3int.rate_limit[cb](cb_param);
3953   //console.log(cb +' need_shift='+ need_shift +' shiftKey='+ evt.shiftKey);
3954	if (!need_shift || evt.shiftKey)
3955	   evt.preventDefault();
3956}
3957
3958function w3_slider_wheel(cb, el, cur, slow, fast)
3959{
3960   el = w3_el(el);
3961   if (!el) return null;
3962   var evt = w3int.rate_limit_evt[cb];
3963	//event_dump(evt, cb, 1);
3964   //console.log(cb +' need_shift='+ w3int.rate_limit_need_shift[cb] +' shiftKey='+ evt.shiftKey +' button='+ evt.button +' buttons='+ evt.buttons);
3965	if (w3int.rate_limit_need_shift[cb] && !evt.shiftKey)
3966	   return null;
3967   var x = evt.deltaX;
3968   var y = evt.deltaY;
3969   var ay = Math.abs(y);
3970   var up_down = ((x < 0 && y <= 0) || (x >= 0 && y < 0));
3971   //console.log(x +' '+ y +' '+ (up_down? 'U':'D') +' '+ w3_id(evt.target));
3972   up_down = up_down? 1 : -1;
3973   var speed = up_down * ((ay < 50)? slow : fast);
3974   //console.log('y='+ y +' '+ speed);
3975   var nval = cur + speed;
3976   //console.log('w3_wheel '+ cb +' min='+ el.min +' max='+ el.max +' step='+ el.step +' cur='+ cur +' speed='+ speed +' nval='+ nval);
3977   return w3_clamp(nval, +el.min, +el.max);
3978}
3979
3980//function w3int_slider_mobile(evt) { evt.preventDefault(); }
3981
3982function w3_slider(psa, label, path, val, min, max, step, cb, cb_param)
3983{
3984   var dump = psa.includes('w3-dump');
3985	var id = w3_add_id(path);
3986	var inline = psa.includes('w3-label-inline');
3987	var bold = !psa.includes('w3-label-not-bold');
3988	var spacing = (label != '' && inline)? 'w3-margin-L-8' : '';
3989	if (inline) spacing += ' w3-margin-left';
3990
3991	value = (val == null)? '' : w3_strip_quotes(val);
3992	value = 'value='+ dq(value);
3993	cb_param = cb_param || 0;
3994	var oc = ' oninput="w3int_slider_change(event, 0, '+ sq(path) +', '+ sq(cb) +', '+ sq(cb_param) +')"';
3995	// change event fires when the slider is done moving
3996	var os = ' onchange="w3int_slider_change(event, 1, '+ sq(path) +', '+ sq(cb) +', '+ sq(cb_param) +')"';
3997   //var om = kiwi_isMobile()? ' ontouchstart="w3int_slider_mobile(event)"' : '';
3998   
3999   var ow = '';
4000   if (psa.includes('w3-wheel')) {
4001      var need_shift = psa.includes('w3-wheel-shift')? 1:0;
4002      var cb_wheel = removeEnding(cb, '_cb') + '_wheel_cb';
4003      ow = ' onwheel="w3int_slider_wheel(event, '+ sq(path) +', '+ sq(cb_wheel) +', '+ sq(cb_param) +', '+ sq(need_shift) +')"';
4004      w3int.rate_limit[cb_wheel] = kiwi_rateLimit(cb_wheel, 170 /* msec */);
4005   }
4006
4007   var psa3 = w3_psa3(psa);
4008   var psa_outer = w3_psa(psa3.left, inline? 'w3-inline-flex':'');
4009   var psa_label = w3_psa_mix(psa3.middle, (label != '' && bold)? 'w3-bold':'');
4010	var psa_inner = w3_psa(psa3.right, w3_sb(id, spacing), '', value +
4011      ' type="range" min='+ dq(min) +' max='+ dq(max) +' step='+ dq(step) + oc + os + ow);
4012
4013	var s = (label != '') ?
4014         ('<div '+ psa_outer +'>' +
4015            w3int_label(psa_label, label, path) +
4016            // need <br> because both <label> and <input-range> are inline elements
4017            ((!inline && label != '')? '<br>':'') +
4018            '<input '+ psa_inner +'>' +
4019         '</div>')
4020      :
4021         ('<input '+ psa_inner +'>');
4022
4023	// run the callback after instantiation with the initial control value
4024	if (cb)
4025		setTimeout(function() {
4026			//console.log('w3_slider: initial callback: '+ cb +'('+ sq(path) +', '+ val +')');
4027			w3_call(cb, path, val, /* complete */ true, /* first */ true, cb_param);
4028		}, 500);
4029
4030	if (dump) console.log(s);
4031	return s;
4032}
4033
4034function w3_slider_set(path, val, cb)
4035{
4036   w3_set_value(path, val);
4037   w3_call(cb, path, val, /* complete */ true, /* first */ false);
4038}
4039
4040function w3_slider_setup(path, min, max, step, val)
4041{
4042   var el = w3_el(path);
4043   if (!el) return el;
4044   //console.log('w3_slider_setup path='+ path +' min='+ min +' max='+ max +' step='+ step +' val='+ val);
4045   el.min = min;
4046   el.max = max;
4047   el.step = step;
4048   el.value = val;
4049   return el;
4050}
4051
4052// used when current value should come from config param
4053function w3_slider_get_param(psa, label, path, min, max, step, cb, cb_param, init_val)
4054{
4055   var update_path_var = psa.includes('w3-update');
4056   var save = psa.includes('w3-defer')? EXT_SAVE_DEFER : undefined;
4057	var cur_val = ext_get_cfg_param(path, (init_val == undefined)? 0 : init_val, save, update_path_var);
4058	//console.log('w3_slider_get_param path='+ path +' update_path_var='+ update_path_var +' init_val='+ init_val +' cur_val='+ cur_val +' psa='+ psa);
4059   return w3_slider(psa, label, path, cur_val, min, max, step, cb, cb_param);
4060}
4061
4062
4063////////////////////////////////
4064// menu (Kiwi menu)
4065////////////////////////////////
4066
4067function w3_menu(psa, cb)
4068{
4069	var id = psa.replace(/\s+/g, ' ').split(' |')[0];
4070   cb = cb || '';
4071   //console.log('w3_menu id='+ id +' psa='+ psa);
4072
4073   // including mouseup here allows the button to continue to be pressed after the hold
4074   // and slid over the menu item to make a selection
4075   var slide_over = psa.includes('w3-slide-over')? 'onmouseup' : 'onclick';
4076   var onclick = slide_over +'="w3int_menu_onclick(event, '+ sq(id) +', '+ sq(cb) +')"' +
4077      ' oncontextmenu="w3int_menu_onclick(event, '+ sq(id) +', '+ sq(cb) +')"';
4078	var p = w3_psa(psa, 'w3-menu w3-menu-container w3-round-large', '', onclick);
4079   var s = '<div '+ p +'></div>';
4080   //console.log('w3_menu s='+ s);
4081   w3_append_innerHTML('id-w3-misc-container', s);
4082}
4083
4084// menu items can be in argument list or passed as an array
4085function w3_menu_items(id, arr, max_vis, selected)
4086{
4087   //console.log('w3_menu_items id='+ id);
4088   if (w3int.menu_debug) canvas_log('w3_menu_items cur='+ w3int.menu_cur_id);
4089   w3_menu_close('open');     // close any existing first
4090	w3int.menu_cur_id = id;
4091   
4092   var s = '';
4093   var i, idx = 0, prop, attr;
4094   var items;
4095
4096   if (isArray(arr)) {
4097      items = arr;
4098   } else {
4099      // isObject(arr)
4100      items = [];
4101      for (i=1; i < arguments.length; i++)
4102         items.push(arguments[i]);
4103   }
4104
4105   if (w3int.menu_debug) canvas_log('#='+ items.length);
4106   for (i=0; i < items.length; i++) {
4107   if (w3int.menu_debug) canvas_log('i='+ i +' '+ items[i]);
4108      if (items[i] == null) continue;
4109      prop = 'w3-menu ';
4110      attr = 'id='+ dq(idx);
4111
4112      if (items[i] == '<hr>') {
4113         prop += 'w3-menu-item-hr';
4114         idx++;
4115      } else
4116      if (isString(items[i]) && items[i].charAt(0) == '!') {    // first char == '!' hack to disable menu item
4117         prop += 'w3-menu-item-disabled';
4118         items[i] = items[i].substr(1);
4119         idx++;
4120      } else {
4121         prop += 'w3-menu-item';
4122         idx++;
4123         if (!kiwi_isMobile()) prop += ' w3-menu-item-hover';
4124         if (isArg(selected) && selected[i])
4125            prop += ' w3-menu-item-selected';
4126      }
4127      var psa = w3_psa_mix('', prop, '', attr);
4128      s += w3_div(psa, items[i]);
4129   }
4130   
4131   if (w3int.menu_debug) canvas_log('s#='+ s.length);
4132   //console.log(s);
4133   var el = w3_el(id);
4134   el.innerHTML = s;
4135   if (w3int.menu_debug)
4136      canvas_log('BUILD');
4137   
4138   // NB: If "overflow-y: auto" is done instead of "overflow-y: 
4138scroll" (via w3-scroll-always-y)
4139   // then the scrollbar will intrude into the width of the menu elements and cause the longest
4140   // to lose right padding.
4141   max_vis = max_vis || items.length;
4142   if (items.length > max_vis) {
4143      el.style.height = px((max_vis * (4 + 19.5 + 4)) + 2*8);
4144      w3_add(el, 'w3-scroll-always-y');
4145   } else {
4146      el.style.height = '';
4147      w3_remove(el, 'w3-scroll-always-y');
4148   }
4149   
4150   // ":hover" highlighting doesn't work on mobile devices -- do it manually.
4151   // But NOT if the menu is long enough to be scrollable since there is no
4152   // gesture to distinguish mousing from scrolling, as with desktop.
4153   if (kiwi_isMobile()) {
4154      if (kiwi_util.mobile_laptop_test) console.log('w3_menu_items MOBILE '+ id);
4155      w3_iterate_children(id, function(el, i) {
4156         el.addEventListener('touchmove', w3int_menu_touch_move, w3.BUBBLING);
4157         if (kiwi_util.mobile_laptop_test)
4158            el.addEventListener('mousemove', w3int_menu_mouse_move, w3.BUBBLING);
4159      });
4160   }
4161}
4162
4163// for testing
4164function w3int_menu_mouse_move(ev)
4165{
4166   var x = Math.round(ev.pageX);
4167   var y = Math.round(ev.pageY);
4168   w3int_menu_move('MMM', x, y);
4169}
4170
4171function w3int_menu_touch_move(ev)
4172{
4173   //event_dump(ev, 'w3int_menu_touch_move');
4174   var x = Math.round(ev.touches[0].pageX);
4175   var y = Math.round(ev.touches[0].pageY);
4176   w3int_menu_move('MTM', x, y);
4177}
4178
4179function w3int_menu_move(which, x, y)
4180{
4181   //console.log('w3int_menu_move '+ which);
4182   var el = document.elementFromPoint(x, y);
4183   if (!el) return;
4184   if (w3_isScrollingY(w3int.menu_cur_id)) return;
4185   var rtn = (!el || !w3_contains(el, 'w3-menu-item'));
4186   //console.log(which +' '+ x +':'+ y +':'+ el.id);
4187   //var el2 = w3_el(w3int.menu_cur_id);
4188   //console.log(el2.scrollTop +':'+ el2.scrollHeight +':'+ el2.clientHeight);
4189
4190   if (w3int.prev_menu_hover_el && (rtn || (el != w3int.prev_menu_hover_el))) {
4191      w3int.prev_menu_hover_el.style.background = '';
4192      w3int.prev_menu_hover_el.style.color = '';
4193      if (rtn) {
4194         //if (w3int.menu_debug) canvas_log('R');
4195         return;
4196      }
4197   }
4198
4199   el.style.background = 'rgb(79, 157, 251)';
4200   el.style.color = 'white';
4201   w3int.prev_menu_hover_el = el;
4202}
4203
4204function w3_menu_popup(id, close_func, x, y)
4205{
4206   //console.log('w3_menu_popup id='+ id +' x='+ x +' y='+ y);
4207   var x0 = x, y0 = y;
4208   var el = w3_el(id);
4209	x = x || window.innerWidth/2;
4210	y = y || window.innerHeight/2;
4211
4212	var slop = 16;
4213	el.w3_realigned = false;
4214	
4215	if ((x + el.clientWidth) + slop > window.innerWidth) {
4216	   x = window.innerWidth - el.clientWidth - slop;
4217	   //if (w3int.menu_debug) canvas_log('X:'+ x0 +':'+ y0);
4218	   el.w3_realigned_time = Date.now();
4219	   el.w3_realigned = true;
4220	}
4221   el.style.left = px(x);
4222
4223   var yt = y + el.clientHeight + slop;
4224	//if (w3int.menu_debug) canvas_log('Y:'+ x0 +':'+ y0 +'/'+ yt +'>'+ window.innerHeight +' '+ TF(yt > window.innerHeight));
4225	if (yt > window.innerHeight) {
4226	   y = window.innerHeight - el.clientHeight - slop;
4227	   el.w3_realigned_time = Date.now();
4228	   el.w3_realigned = true;
4229	}
4230   el.style.top = px(y);
4231   //console.log('w3_menu_popup NEW id='+ id +' x='+ x +' y='+ y);
4232
4233   w3_visible(el, true);
4234   if (w3int.menu_debug)
4235      canvas_log('VIS');
4236   w3int.menu_active = true;
4237   el.w3_menu_x = x;
4238
4239	// close menu if escape key while menu is displayed
4240	w3int.menu_close_func = close_func;
4241   w3int.menu_first = true;
4242	window.addEventListener("keyup", w3int_menu_event, w3.BUBBLING);
4243	window.addEventListener("mousedown", w3int_menu_event, w3.BUBBLING);
4244	window.addEventListener("touchstart", w3int_menu_event, w3.BUBBLING);
4245	window.addEventListener("click", w3int_menu_event, w3.BUBBLING);
4246}
4247
4248function w3_menu_active()
4249{
4250   return w3int.menu_active;
4251}
4252
4253function w3int_menu_onclick(ev, id, cb, cb_param)
4254{
4255   if (w3int.menu_debug)
4256      canvas_log('menu_onclick '+ id +' from='+ (cb_param || ev.type) +' cb='+ cb);
4257   //console.info('w3int_menu_onclick id='+ id +' cb='+ cb);
4258   //if (ev != null) event_dump(ev, "MENU", false);
4259   var el = w3_el(id);
4260
4261   // ignore false click from menu re-alignment (only if click is recent enough)
4262   //console.log('w3int_menu_onclick realigned='+ el.w3_realigned);
4263   if (el.w3_realigned) {
4264      el.w3_realigned = false;
4265      var when = Date.now() - el.w3_realigned_time;
4266      if (when < 50) {
4267         if (w3int.menu_debug) canvas_log('XRe'+ when);
4268         //console.log('w3int_menu_onclick: cancelEvent()');
4269         return cancelEvent(ev);
4270      }
4271      if (w3int.menu_debug) canvas_log('XOK'+ when);
4272   }
4273
4274   //if (cb == null) kiwi_trace('w3int_menu_onclick');
4275   if (ev != null && cb != null) {
4276      var _id = ev.target.id;
4277      var idx = +_id;
4278      if (_id == '' || isNaN(idx)) _id = ev.target.parentNode.id;
4279      idx = +_id;
4280      if (_id == '' || isNaN(idx)) idx = -1;
4281      //console.log('w3int_menu_onclick CALL idx='+ idx);
4282      if (w3int.menu_debug)
4283         canvas_log('CALL: '+ idx);
4284      if (idx == -1) return;     // clicked/touched top/bottom border -- don't dismiss menu
4285      w3_call(cb, idx, el.w3_menu_x, cb_param, ev);
4286   }
4287
4288   w3_visible(el, false);
4289   //console.log('w3int_menu_onclick NOT_VIS');
4290   if (w3int.menu_debug)
4291      canvas_log('NOT_VIS');
4292   w3int.menu_active = false;
4293   window.removeEventListener("keyup", w3int_menu_event, w3.BUBBLING);
4294   window.removeEventListener("mousedown", w3int_menu_event, w3.BUBBLING);
4295   window.removeEventListener("touchstart", w3int_menu_event, w3.BUBBLING);
4296   window.removeEventListener("click", w3int_menu_event, w3.BUBBLING);
4297
4298   // allow right-button to select menu items
4299	if (ev != null) {
4300      if (w3int.menu_debug) canvas_log('Enull');
4301	   return cancelEvent(ev);
4302	}
4303}
4304
4305// close menu if caller's close_func() returns true
4306function w3int_menu_event(evt)
4307{
4308   if (w3int.menu_debug) canvas_log('ME '+ evt.type);
4309   var el = w3_el(w3int.menu_cur_id);
4310   //console.info('w3int_menu_event');
4311   //console.log(el);
4312   //event_dump(evt, 'MENU-EVT', true);
4313
4314   if (el && w3int.menu_close_func) {
4315      //console.log(w3int.menu_close_func);
4316      var close = w3int.menu_close_func(evt, w3int.menu_first);
4317      //console.log('w3int_menu_event CLOSE close='+ close +' first='+ w3int.menu_first);
4318      w3int.menu_first = false;
4319      if (w3int.menu_debug) canvas_log('CLOSE='+ close);
4320      if (close) w3_menu_close(evt.type);
4321   }
4322   //else canvas_log('no w3_menu_close()');
4323}
4324
4325function w3_menu_close(from)
4326{
4327   //console.info('w3_menu_close');
4328   if (w3int.menu_debug) canvas_log('w3_menu_close '+ from +' id='+ w3int.menu_cur_id);
4329   if (!w3int.menu_cur_id) return;
4330   var el = w3_el(w3int.menu_cur_id);
4331   if (!el) return;
4332   el.w3_realigned = false;
4333   w3int_menu_onclick(null, w3int.menu_cur_id, null, 'menu_close');
4334   w3int.menu_cur_id = null;
4335}
4336
4337
4338////////////////////////////////
4339// standard callbacks -- only set var
4340////////////////////////////////
4341
4342function w3_num_cb(path, val)
4343{
4344	var v = parseFloat(val);
4345	if (val == '') v = 0;
4346	if (isNaN(v)) {
4347	   //console.log('w3_num_cb path='+ path);
4348	   w3_set_value(path, 0);
4349	   v = 0;
4350	}
4351	//console.log('w3_num_cb: path='+ path +' val='+ val +' v='+ v);
4352	setVarFromString(path, v);
4353}
4354
4355function w3_bool_cb(path, val)
4356{
4357	v = +val;
4358	//console.log('w3_bool_cb: path='+ path +' val='+ val +' v='+ v);
4359	setVarFromString(path, v);
4360}
4361
4362function w3_string_cb(path, val)
4363{
4364	//console.log('w3_string_cb: path='+ path +' val='+ val);
4365	setVarFromString(path, val.toString());
4366}
4367
4368function w3_select_cb(path, val)
4369{
4370	val = +val;
4371	//console.log('w3_select_cb: path='+ path +' val='+ val);
4372	setVarFromString(path, val);
4373}
4374
4375
4376
4377////////////////////////////////
4378// standard callbacks -- set cfg and var
4379////////////////////////////////
4380
4381function w3_num_set_cfg_cb(path, val, first)
4382{
4383	var v = parseFloat(val);
4384	if (isNaN(v)) v = 0;
4385	
4386	// if first time don't save, otherwise always save
4387	var save = isArg(first)? (first? false : true) : true;
4388	ext_set_cfg_param(path, v, save);
4389}
4390
4391function w3_int_set_cfg_cb(path, val, first)
4392{
4393	var v = parseInt(val);
4394	if (isNaN(v)) v = 0;
4395	w3_set_value(path, v);     // remove any fractional or non-number portion from field
4396	
4397	// if first time don't save, otherwise always save
4398	var save = isArg(first)? (first? false : true) : true;
4399	ext_set_cfg_param(path, v, save);
4400}
4401
4402// limit precision using callback spec: 'w3_float_set_cfg_cb|prec'
4403function w3_float_set_cfg_cb(path, val, first, cb_a)
4404{
4405   var prec = -1;    // default to no precision limiting applied
4406	//console.log('admin_float_cb '+ path +'='+ val +' cb_a.len='+ cb_a.length);
4407	if (isArray(cb_a) && cb_a.length >= 2) {
4408	   prec = +(cb_a[1]);
4409	   if (isNaN(prec)) prec = -1;
4410	   //console.log('admin_float_cb prec='+ prec);
4411	} else
4412	if (isNumber(cb_a)) {
4413	   prec = cb_a;
4414	   if (isNaN(prec)) prec = -1;
4415	}
4416	val = parseFloat(val);
4417	if (isNaN(val)) {
4418	   // put old value back
4419	   val = ext_get_cfg_param(path);
4420	} else {
4421	   if (prec != -1) {
4422         var s = val.toFixed(prec);    // NB: .toFixed() does rounding
4423         //console.log('admin_float_cb val('+ prec +')='+ s);
4424         val = +s;
4425      }
4426	   // if first time don't save, otherwise always save
4427	   var save = isArg(first)? (first? false : true) : true;
4428	   ext_set_cfg_param(path, val, save);
4429	}
4430   w3_set_value(path, val);   // remove any non-numeric part from field
4431}
4432
4433function w3_bool_set_cfg_cb(path, val, first)
4434{
4435	var v;
4436	if (val == true || val == 1) v = true; else
4437	if (val == false || val == 0) v = false; else
4438	   v = false;
4439	
4440	// if first time don't save, otherwise always save
4441	var save = isArg(first)? (first? false : true) : true;
4442	ext_set_cfg_param(path, v, save);
4443}
4444
4445function w3_string_old_set_cfg_cb(path, val, first)
4446{
4447	console.log('w3_string_old_set_cfg_cb: path='+ path +' '+ typeof(val) +' <'+ val +'> first='+ first);
4448	
4449	// if first time don't save, otherwise always save
4450	var save = isArg(first)? (first? false : true) : true;
4451	ext_set_cfg_param(path, encodeURIComponent(val.toString()), save);
4452}
4453
4454function w3_string_set_cfg_cb(path, val, first)
4455{
4456	//console.log('w3_string_set_cfg_cb: path='+ path +' '+ typeof(val) +' <'+ val +'> first='+ first);
4457   w3_json_set_cfg_cb(path, val, first);
4458}
4459
4460function w3_json_set_cfg_cb(path, val, first)
4461{
4462	//console.log('w3_json_set_cfg_cb: path='+ path +' '+ typeof(val) +' <'+ val +'> first='+ first);
4463
4464	// verify that value is valid JSON
4465	var v = val;
4466	if (isString(v)) v = JSON.stringify(v);
4467   try {
4468      JSON.parse(v);
4469   } catch(ex) {
4470      console.log('w3_json_set_cfg_cb: path='+ path +' '+ typeof(val) +' <'+ val +'> first='+ first);
4471      console.log('w3_json_set_cfg_cb: DANGER value did not parse as JSON!');
4472      return;
4473   }
4474
4475	// if first time don't save, otherwise always save
4476	var save = isArg(first)? (first? false : true) : true;
4477	ext_set_cfg_param(path, val, save);
4478}
4479
4480// like w3_string_set_cfg_cb(), but enforces specification of leading 'http://' or 'https://'
4481function w3_url_set_cfg_cb(path, val, first)
4482{
4483	//console.log('w3_url_set_cfg_cb: path='+ path +' '+ typeof(val) +' <'+ val +'> first='+ first);
4484	if (val != '' && !val.startsWith('http://') && !val.startsWith('https://')) val = 'http://'+ val;
4485	w3_string_set_cfg_cb(path, val, first);
4486	w3_set_value(path, val);
4487}
4488
4489
4490// path is structured to be: [el_name][sep][idx]
4491// e.g. [id-dx.o.ty][_][123]
4492function w3_remove_trailing_index(path, sep)
4493{
4494   sep = sep || '-';    // optional separator, e.g. if negative indicies are used
4495   var re = new RegExp('(.*)'+ sep +'([-]?[0-9]+)');
4496	var el = re.exec(path);      // remove trailing -nnn
4497   //console.log('w3_remove_trailing_index path='+ path +' el='+ el);
4498	var idx;
4499	if (!el) {
4500	   idx = null;
4501	   el = path;
4502	} else {
4503	   idx = el[2];
4504	   el = el[1];
4505	}
4506	return { el:el, idx:idx };
4507}
4508
4509var w3_MENU_NEXT = 1;
4510var w3_MENU_PREV = -1;
4511
4512// cb_param = { dir:w3_MENU_{NEXT,PREV}, id:'...', func:'...', (optional, to skip non-numeric menu items) isNumeric:true }
4513function w3_select_next_prev_cb(path, cb_param, first)
4514{
4515   var val;
4516	if (dbgUs) console.log('w3_select_next_prev_cb cb_param='+ decodeURIComponent(cb_param));
4517   var cbp = w3_cb_param_decode(cb_param);
4518   if (!isObject(cbp)) {
4519	   console.log('w3_select_next_prev_cb cb_param is not an object');
4520	   return;
4521   }
4522	if (dbgUs) console.log('w3_select_next_prev_cb dir='+ cbp.dir +' id='+ cbp.id);
4523	
4524   // if any menu has a selection value then select next/prev (if any)
4525   var prev = 0, capture_the_next = 0, captured_next = 0, captured_prev = 0;
4526   var menu_s, el;
4527   
4528   for (var i = 0; (el = w3_el(menu_s = cbp.id + i)) != null; i++) {
4529      val = el.value;
4530      if (dbgUs) console.log('menu_s='+ menu_s +' value='+ val);
4531      if (val == -1) continue;
4532      
4533      w3_select_enum(menu_s, function(option) {
4534         var content_check = (isUndefined(cbp.isNumeric) || (cbp.isNumeric == true && isNumber(parseInt(option.innerText))));
4535	      if (option.disabled || !content_check) return;
4536         if (dbgUs) console.log('consider val='+ option.value +' \"'+ option.innerText +'\" content_check='+ content_check);
4537         if (capture_the_next) {
4538            captured_next = option.value;
4539            capture_the_next = 0;
4540         }
4541         if (option.value === val) {
4542            captured_prev = prev;
4543            capture_the_next = 1;
4544         }
4545         prev = option.value;
4546      });
4547      break;
4548   }
4549
4550	if (dbgUs) console.log('i='+ i +' captured_prev='+ captured_prev +' captured_next='+ captured_next);
4551	val = 0;
4552	if (cbp.dir == w3_MENU_NEXT && captured_next) val = captured_next;
4553	if (cbp.dir == w3_MENU_PREV && captured_prev) val = captured_prev;
4554	if (dbgUs) console.log('w3_select_next_prev_cb val='+ val);
4555	//if (val)
4556	   w3_call(cbp.func, menu_s, val, first);
4557}
4558
4559
4560////////////////////////////////
4561// tables
4562////////////////////////////////
4563
4564// caller can choose more specific table type, e.g. w3-table-fixed
4565// FIXME: deprecated, only still used in admin GPS
4566function w3_table(psa)
4567{
4568	var p = w3_psa(psa, 'w3-table w3-table-default');
4569	var s = '<table '+ p +'>';
4570		for (var i=1; i < arguments.length; i++) {
4571			s += arguments[i];
4572		}
4573	s += '</table>';
4574	//console.log(s);
4575	return s;
4576}
4577
4578function w3_table_head(psa)
4579{
4580	var p = w3_psa(psa);
4581	var s = '<thead '+ p +'>';
4582		for (var i=1; i < arguments.length; i++) {
4583	      if (arguments[i] == null) continue;
4584			s += arguments[i];
4585		}
4586	s += '</thead>';
4587	//console.log(s);
4588	return s;
4589}
4590
4591function w3_table_body(psa)
4592{
4593	var p = w3_psa(psa);
4594	var s = '<tbody '+ p +'>';
4595		for (var i=1; i < arguments.length; i++) {
4596	      if (arguments[i] == null) continue;
4597			s += arguments[i];
4598		}
4599	s += '</tbody>';
4600	//console.log(s);
4601	return s;
4602}
4603
4604function w3_table_heads(psa)
4605{
4606	var p = w3_psa(psa, 'w3-table-head');
4607	var s = '';
4608	for (var i=1; i < arguments.length; i++) {
4609	   if (arguments[i] == null) continue;
4610		s += '<th '+ p +'>';
4611		s += arguments[i];
4612		s += '</th>';
4613	}
4614	//console.log(s);
4615	return s;
4616}
4617
4618function w3_table_row(psa)
4619{
4620	var p = w3_psa(psa, 'w3-table-row');
4621	var s = '<tr '+ p +'>';
4622		for (var i=1; i < arguments.length; i++) {
4623	      if (arguments[i] == null) continue;
4624			s += arguments[i];
4625		}
4626	s += '</tr>';
4627	//console.log(s);
4628	return s;
4629}
4630
4631function w3_table_cells(psa)
4632{
4633	var p = w3_psa(psa, 'w3-table-cell');
4634	var s = '';
4635	for (var i=1; i < arguments.length; i++) {
4636	   if (arguments[i] == null) continue;
4637		s += '<td '+ p +'>';
4638		s += arguments[i];
4639		s += '</td>';
4640	}
4641	//console.log(s);
4642	return s;
4643}
4644
4645
4646////////////////////////////////
4647// containers
4648////////////////////////////////
4649
4650// If any psa prop or style is of the form N:{prop,style} then only use prop or style if N == current arg number
4651// e.g.
4652// w3_inline('foo/bar 1:baz 2:|zzz:123', w3_div('zero'), w3_div('one'), w3_div('two')) =>
4653// <div class="foo">
4654//    <div class="bar zero"> ... </div>
4655//    <div class="bar one baz"> ... </div>
4656//    <div class="bar two" style="zzz:123"> ... </div>
4657// </div>
4658// This is a generalization of the w3-btn-right feature of w3int_button()
4659
4660function w3_inline(psa, attr)
4661{
4662	var narg = arguments.length;
4663   var dump = psa.includes('w3-dump');
4664   var div_or_span = psa.includes('w3-span')? 'span' : 'div';
4665   var enclosing = !psa.includes('w3-no-encl');
4666   if (dump) console.log(arguments);
4667   
4668   if (psa == '' && attr == '' && narg > 2) {
4669      console.log('### w3_inline OLD API DEPRECATED');
4670      var args = Array.prototype.splice.call(arguments, 0);
4671      args.splice(0, 1);
4672      return w3_inline.apply(null, args);
4673   } else {
4674      var psa3 = w3_psa3(psa);
4675      var psa_outer = w3_psa(psa3.middle, 'w3-inline-flex');
4676      var psa_inner = w3_psa(psa3.right);
4677      if (dump) console.log('w3_inline psa_outer='+ psa_outer +' psa_inner='+ psa_inner +' enclosing='+ enclosing);
4678      var s = '<'+ div_or_span +' w3d-inlo '+ psa_outer +'>';
4679
4680      // for each of the inline items
4681      for (var i = 1; i < narg; i++) {
4682         var a = arguments[i];
4683         if (!a) continue;
4684         if (dump) console.log('w3_inline i='+ i +' a='+ a);
4685         
4686         // merge a pseudo psa specifier into the next argument
4687         // e.g. 'w3-*', w3_*('w3-psa') => w3_*('w3-* w3-psa')
4688         var psa_merge = false;
4689         if (a.startsWith('w3-') || a.startsWith('id-')) {
4690            psa = w3_psa(psa3.right, a);
4691            i++;
4692            a = arguments[i];
4693            psa_merge = true;
4694         } else {
4695            if (psa_inner != '') {
4696               // apply N:{prop,style} to corresponding inline element if present
4697               var psa3r = '';
4698               var a1 = psa3.right.split(' ');
4699               a1.forEach(
4700                  function(p1) {
4701                     if (dump) console.log('N:{prop,style} i='+ (i-1) +' p1=');
4702                     var a2 = p1.split(':');
4703                     if (dump) console.log(a2);
4704                     if (a2.length >= 2) {
4705                        var n = +a2[0];
4706                        var match = (n == (i-1));
4707                        if (dump) console.log('i='+ i +' n='+ n +' '+ match);
4708                        var isBar = (a2[1][1] == '|');
4709                        a2.shift();
4710                        rem = a2.join(':');
4711                        var xprop = (match && !isBar)? rem : '';
4712                        var xstyle = (match && isBar)? rem.slice(1) : '';
4713                        psa3r = w3_sb(psa3r, xprop, xstyle);
4714                     } else {
4715                        psa3r = w3_sb(psa3r, p1);
4716                     }
4717                  }
4718               );
4719               if (dump) console.log('psa3r='+ psa3r);
4720               psa = w3_psa(psa3r);
4721               if (dump) console.log('psa='+ psa);
4722            } else {
4723               psa = '';
4724            }
4725         }
4726         
4727         // If the psa is empty, and the arg is a div, don't wrap it with our usual enclosing div.
4728         // This solves the "w3_inline() + w3-hide" problem where our extra div
4729         // added by w3_inline isn't the one with the w3-hide, and causes unwanted spacing when
4730         // using w3_inline('w3-halign-space-between/').
4731         if (psa3.right == '' && !psa_merge && a.startsWith('<div ')) {
4732            if (dump) console.log('FINAL-1 '+ a);
4733            s += a;
4734         } else {
4735            if (enclosing) {
4736               a = '<'+ div_or_span +' w3d-inli-'+ w3_sb(i-1, psa) +'>'+ a + '</'+ div_or_span +'>';
4737               if (dump) console.log('FINAL-2 '+ a);
4738               s += a;
4739            } else {
4740               if (dump) console.log('FINAL-3 '+ a);
4741               s += a;
4742            }
4743         }
4744      }
4745      s += '</'+ div_or_span +'>';
4746      if (dump) console.log(s);
4747      return s;
4748   }
4749}
4750
4751// call w3_inline when parameters need to be accumulated in an array
4752function w3_inline_array(psa, ar)
4753{
4754	ar.unshift(psa);     // add psa to head of array
4755	//console.log(ar);
4756   return w3_inline.apply(null, ar);
4757}
4758
4759function w3_inline_noencl()
4760{
4761   var a = Array.from(arguments);
4762   psa = w3_psa3(a[0]);
4763   a[0] = w3_sb(psa.middle, 'w3-no-encl/') + psa.right;
4764   return w3_inline.apply(null, a);
4765}
4766
4767// see: stackoverflow.com/questions/29885284/how-to-set-a-fixed-width-column-with-css-flexbox
4768//
4769// If any psa prop is of the form N:prop then only use prop if N == current arg number
4770// e.g.
4771// w3_inline_percent('foo/bar 1:baz', w3_div('zero'), w3_div('one'), w3_div('two')) =>
4772// <div class="foo">
4773//    <div class="bar zero"> ... </div>
4774//    <div class="bar baz one"> ... </div>
4775//    <div class="bar two"> ... </div>
4776// </div>
4777// This is a generalization of the w3-btn-right feature of w3int_button()
4778
4779function w3_inline_percent(psa)
4780{
4781	var narg = arguments.length;
4782   var dump = psa.includes('w3-dump');
4783   var psa3 = w3_psa3(psa);
4784   var psa_outer = w3_psa(psa3.middle, 'w3-inline-flex');
4785   var psa_inner = w3_psa(psa3.right);
4786   if (dump) console.log('w3_inline_percent psa_outer='+ psa_outer +' psa_inner='+ psa_inner);
4787	var total = 0;
4788	var prop = psa.includes('w3-debug')? 'w3_debug ' : '';
4789	var last_halign_end = psa.includes('w3-last-halign-end');
4790	var s = '<div w3d-inlpo '+ psa_outer +'>';
4791		for (var i = 1; i < narg; i += 2) {
4792		   var im1d2 = (i-1)/2;
4793		   var style;
4794		   if (i+1 < narg) {
4795		      style = 'flex-basis:'+ arguments[i+1] +'%';
4796		      total += arguments[i+1];
4797		   } else {
4798		      style = 'flex-basis:'+ (100 - total) +'%';
4799		   }
4800		   if (i == narg-1 && last_halign_end) prop += 'w3-halign-end';
4801         if (psa_inner != '') {
4802            var psa3r = '';
4803            var a1 = psa3.right.split(' ');
4804            a1.forEach(
4805               function(p1) {
4806                  if (dump) console.log('N:prop i='+ (im1d2-0) +' p1=');
4807                  var a2 = p1.split(':');
4808                  if (dump) console.log(a2);
4809                  if (a2.length == 2) {
4810                     var n = +a2[0];
4811                     if (dump) console.log('i='+ i +' n='+ n +' '+ (n == (im1d2-0)));
4812                     psa3r = w3_sb(psa3r, (n == (im1d2-0))? a2[1] : '');
4813                  } else {
4814                     psa3r = w3_sb(psa3r, p1);
4815                  }
4816               }
4817            );
4818            if (dump) console.log('w3_inline_percent prop='+ prop +' style='+ style);
4819            psa = w3_psa(psa3r, prop, style);
4820         } else {
4821			   psa = w3_psa(psa3.right, prop, style);
4822         }
4823			s += '<div w3d-inlpi-'+ w3_sb(im1d2, psa) +'>'+ (arguments[i] || '') + '</div>';
4824		}
4825	s += '</div>';
4826	if (dump) console.log(s);
4827	return s;
4828}
4829
4830function w3_inline_percent_set(id, p)
4831{
4832   var el = w3_el(id);
4833   el.style.flexBasis = pct(p);
4834}
4835
4836/*
4837// old version using div width:%
4838function w3_inline_percent_old(psa)
4839{
4840   var psa3 = w3_psa3(psa);
4841   var psa_outer = w3_psa(psa3.middle, 'w3-inline-flex');
4842	var narg = arguments.length;
4843	var total = 0;
4844	var s = '<div w3d-inlpo '+ psa_outer +'>';
4845		for (var i = 1; i < narg; i += 2) {
4846		   var style;
4847		   if (i+1 < narg) {
4848		      style = 'width:'+ arguments[i+1] +'%';
4849		      total += arguments[i+1];
4850		   } else {
4851		      style = 'width:'+ (100 - total) +'%';
4852		   }
4853			s += '<div w3d-inlpi-'+ ((i-1)/2) +' '+ w3_psa(psa3.right, '', style) +'>'+ arguments[i] + '</div>';
4854		}
4855	s += '</div>';
4856	//console.log(s);
4857	return s;
4858}
4859*/
4860
4861function w3_div(psa)
4862{
4863   var p = w3_psa(psa);
4864	var s = '<div w3d-div '+ p +'>';
4865	var narg = arguments.length;
4866		for (var i=1; i < narg; i++) {
4867			s += arguments[i];
4868		}
4869	s += '</div>';
4870	//console.log(s);
4871	return s;
4872}
4873
4874function w3_span(psa)
4875{
4876   var div_or_span = psa.includes('w3-span')? 'span' : 'div';
4877   var p = w3_psa(psa, 'w3-show-span');
4878	var s = '<'+ div_or_span +' w3d-span '+ p +'>';
4879	var narg = arguments.length;
4880		for (var i=1; i < narg; i++) {
4881			s += arguments[i];
4882		}
4883	s += '</'+ div_or_span +'>';
4884	//console.log(s);
4885	return s;
4886}
4887
4888function w3_divs(psa, attr)
4889{
4890   var narg = arguments.length;
4891   var s, args;
4892
4893   if (psa == '' && attr == '') {
4894      console.log('### w3_divs OLD API 1 DEPRECATED');
4895      //console.log('prop=<'+ psa +'> attr=<'+ attr +'>');
4896      //kiwi_trace();
4897      s = w3_div.apply(null, Array.prototype.splice.call(arguments, 1));
4898      //console.log(s);
4899      return s;
4900   } else
4901   if (psa != '' && attr == '' && narg > 2) {
4902      console.log('### w3_divs OLD API 2 DEPRECATED');
4903      //console.log('prop=<'+ psa +'> attr=<'+ attr +'>');
4904      //kiwi_trace();
4905      args = Array.prototype.splice.call(arguments, 0);
4906      args.splice(1, 1);
4907      s = w3_div.apply(null, args);
4908      //console.log(s);
4909      return s;
4910   } else
4911   if (psa == '' && attr && attr.startsWith('w3-') && narg > 2) {
4912      console.log('### w3_divs OLD API 3 DEPRECATED');
4913      //console.log('prop=<'+ psa +'> attr=<'+ attr +'>');
4914      //kiwi_trace();
4915      args = Array.prototype.splice.call(arguments, 0);
4916      args.splice(0, 1);
4917      s = w3_divs.apply(null, args);
4918      //console.log(s);
4919      return s;
4920   } else
4921   if (psa != '' && attr && attr.startsWith('w3-') && narg > 2) {
4922      console.log('### w3_divs OLD API 4 DEPRECATED');
4923      //console.log('prop=<'+ psa +'> attr=<'+ attr +'>');
4924      //kiwi_trace();
4925      args = Array.prototype.splice.call(arguments, 2);
4926      args.splice(0, 0, arguments[0] +'/'+ arguments[1]);
4927      s = w3_divs.apply(null, args);
4928      //console.log(s);
4929      return s;
4930   } else {
4931      var psa3 = w3_psa3(psa);
4932      var psa_outer = w3_psa(psa3.middle);
4933      var psa_inner = w3_psa(psa3.right);
4934      s = '<div w3d-divso '+ psa_outer +'>';
4935         for (var i=1; i < narg; i++) {
4936            s += '<div w3d-divsi-'+ (i-1) +' '+ psa_inner +'>'+ arguments[i] + '</div>';
4937         }
4938      s += '</div>';
4939      //console.log(s);
4940      return s;
4941   }
4942}
4943
4944function w3_col_percent(psa)
4945{
4946   var psa3 = w3_psa3(psa);
4947   var psa_outer = w3_psa(psa3.middle, 'w3-row');
4948	var narg = arguments.length;
4949	var s = '<div w3d-colpo '+ psa_outer +'>';
4950		for (var i = 1; i < narg; i += 2) {
4951		   var prop, style;
4952		   if (i+1 < narg) {
4953		      prop = 'w3-col';
4954		      style = 'width:'+ arguments[i+1] +'%';
4955		   } else {
4956		      prop = 'w3-rest';
4957		      style = '';
4958		   }
4959			s += '<div w3d-colpi-'+ ((i-1)/2) +' '+ w3_psa(psa3.right, prop, style) +'>'+ arguments[i] + '</div>';
4960		}
4961	s += '</div>';
4962	//console.log(s);
4963	return s;
4964}
4965
4966// the w3-text makes it inline-block, so no surrounding w3_inline() needed
4967function w3_text(psa, text)
4968{
4969   var style = (psa.includes('w3-padding') || psa.includes('w3-nopad'))? '' : 'padding:0 4px 0 0';
4970   if (!psa.includes('w3-nobk')) style += '; background-color:inherit';
4971	var s = w3_div(w3_psa_mix(psa, 'w3-text', style), text? text:' ');
4972	//console.log(s);
4973	return s;
4974}
4975
4976function w3_code(psa)
4977{
4978   var psa3 = w3_psa3(psa);
4979   var narg = arguments.length;
4980	var s = '<pre '+ w3_psa(psa3.left) +'><code '+ w3_psa(psa3.middle) +'>';
4981		for (var i=1; i < narg; i++) {
4982			s += '<code '+ w3_psa(psa3.right) +'>'+ arguments[i] + '</code>';
4983		}
4984	s += '</code></pre>';
4985	//console.log(s);
4986	return s;
4987}
4988
4989function w3_header(psa, size)
4990{
4991   var psa3 = w3_psa3(psa);
4992   //console.log(psa3);
4993   var narg = arguments.length;
4994	var s = '<header '+ w3_psa(psa3.middle) +'><h'+ size +' '+ w3_psa(psa3.right) +'>';
4995		for (var i=2; i < narg; i++) {
4996			s += arguments[i];
4997		}
4998	s += '</h'+ size +'></header>';
4999	//console.log(s);
5000	return s;
5001}
5002
5003/*
5004function w3_half(prop_row, prop_col, left, right, prop_left, prop_right)
5005{
5006	if (prop_left == undefined) prop_left = '';
5007	if (prop_right == undefined) prop_right = '';
5008
5009	var s =
5010	'<div class="w3-row '+ prop_row +'">' +
5011		'<div class="w3-col w3-half '+ prop_col + prop_left +'">' +
5012			left +
5013		'</div>' +
5014		'<div class="w3-col w3-half '+ prop_col + prop_right +'">' +
5015			(right? right:'') +
5016		'</div>' +
5017	'</div>';
5018	//console.log(s);
5019	return s;
5020}
5021*/
5022
5023function w3_half(prop_row, prop_col, left, right)
5024{
5025   return w3_inline_percent(w3_sbc('/', prop_row, prop_col), left, 50, right);
5026}
5027
5028/*
5029function w3_third(prop_row, prop_col, left, middle, right)
5030{
5031	var s =
5032	'<div class="w3-row '+ prop_row +'">' +
5033		'<div class="w3-col w3-third '+ prop_col +'">' +
5034			left +
5035		'</div>' +
5036		'<div class="w3-col w3-third '+ prop_col +'">' +
5037			(middle? middle:'') +
5038		'</div>' +
5039		'<div class="w3-col w3-third '+ prop_col +'">' +
5040			(right? right:'') +
5041		'</div>' +
5042	'</div>';
5043	//console.log(s);
5044	return s;
5045}
5046*/
5047
5048function w3_third(prop_row, prop_col, left, middle, right)
5049{
5050   return w3_inline_percent(w3_sbc('/', prop_row, prop_col), left, 33, middle, 33, right);
5051}
5052
5053/*
5054function w3_quarter(prop_row, prop_col, left, middleL, middleR, right)
5055{
5056	var s =
5057	'<div class="w3-row '+ prop_row +'">' +
5058		'<div class="w3-col w3-quarter '+ prop_col +'">' +
5059			left +
5060		'</div>' +
5061		'<div class="w3-col w3-quarter '+ prop_col +'">' +
5062			(middleL? middleL:'') +
5063		'</div>' +
5064		'<div class="w3-col w3-quarter '+ prop_col +'">' +
5065			(middleR? middleR:'') +
5066		'</div>' +
5067		'<div class="w3-col w3-quarter '+ prop_col +'">' +
5068			(right? right:'') +
5069		'</div>' +
5070	'</div>';
5071	//console.log(s);
5072	return s;
5073}
5074*/
5075
5076function w3_quarter(prop_row, prop_col, left, middleL, middleR, right)
5077{
5078   return w3_inline_percent(w3_sbc('/', prop_row, prop_col), left, 25, middleL, 25, middleR, 25, right);
5079}
5080
5081function w3_canvas(psa, w, h, opt)
5082{
5083   var s = '';
5084   var pad = w3_opt(opt, 'pad', null);
5085   var pad_top = w3_opt(opt, 'pad_top', 0);
5086   var pad_bot = w3_opt(opt, 'pad_bottom', 0);
5087   if (!isNull(pad)) pad_top = pad_bot = pad;
5088   pad = pad_top + pad_bot;
5089   if (pad) {
5090      //w -= pad*2; h -= pad*2;
5091      s += sprintf(' padding-top:%dpx; padding-bottom:%dpx;', pad_top, pad_bot);
5092   }
5093
5094   var left = w3_opt(opt, 'left', '');
5095   if (left != '') s += sprintf(' left:%dpx;', left);
5096   var right = w3_opt(opt, 'right', '');
5097   if (right != '') s += sprintf(' right:%dpx;', right);
5098   var top = w3_opt(opt, 'top', '');
5099   if (top != '') s += sprintf(' top:%dpx;', top);
5100
5101   s = '<canvas '+ w3_psa(psa, null, 'position:absolute;'+ s, sprintf('width="%d" height="%d"', w, h)) +'></canvas>';
5102	//console.log(s);
5103	return s;
5104}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.