rolling.html: Eliminate pointless `h2' and promote subheadings.
[dep-ui] / dep-ui.js
CommitLineData
ac26861c
MW
1/* -*-javascript-*-
2 *
3 * Dependency-based user interface in a web page.
4 *
5 * (c) 2013 Mark Wooding
6 */
7
8/*----- Licensing notice --------------------------------------------------*
9 *
10 * This program is free software; you can redistribute it and/or
11 * modify it under the terms of the GNU General Public License as
12 * published by the Free Software Foundation; either version 2 of the
13 * License, or (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU Library General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
8a0fc4a2 21 * along with this program; if not, see <https://www.gnu.org/licenses/>.
ac26861c
MW
22 */
23
6cfd4191 24var DEP_UI = {}; (function () {
ac26861c
MW
25
26/*----- Utility functions and classes -------------------------------------*/
27
6cfd4191 28function debug(msg) {
c6150f2f
MW
29 /* Write the string MSG to the `trace' element, if there is one. */
30
31 var e = elt('trace');
32 if (e !== null) e.textContent += msg;
33}
6cfd4191 34DEP_UI.debug = debug;
c6150f2f 35
6cfd4191 36function trap(what, func) {
c6150f2f
MW
37 try {
38 func();
39 } catch (e) {
40 debug('caught exception in ' + what + ': ' + e);
41 throw e;
42 }
43}
6cfd4191 44DEP_UI.trap = trap;
c6150f2f 45
6cfd4191 46function elt(id) {
ac26861c
MW
47 /* Find and return the element with the given ID. */
48 return document.getElementById(id);
49}
6cfd4191 50DEP_UI.elt = elt;
ac26861c 51
6cfd4191 52function add_elt_class(elt, cls) {
ac26861c
MW
53 /* Add the class name CLS to element ELT's `class' attribute. */
54
55 if (!elt.className.match('\\b' + cls + '\\b'))
56 elt.className += ' ' + cls
57}
6cfd4191 58DEP_UI.add_elt_class = add_elt_class;
ac26861c 59
6cfd4191 60function rm_elt_class(elt, cls) {
ac26861c
MW
61 /* Remove the class name CLS from element ELT's `class' attribute. */
62
63 elt.className = elt.className.replace(
64 new RegExp ('\\s*\\b' + cls + '\\b\\s*'), ' ');
65}
6cfd4191 66DEP_UI.rm_elt_class = rm_elt_class;
ac26861c
MW
67
68/* A gadget which can arrange to perform an idempotent action (the FUNC
69 * argument) again `soon'.
70 */
6cfd4191 71function Soon(func) {
ac26861c
MW
72 this.timer = null;
73 this.func = func;
74}
75Soon.prototype = {
76 kick: function () {
77 /* Make sure the function is called again soon. If we've already been
78 * kicked, then put off the action for a bit more (in case things need
79 * time to settle).
80 */
81
82 var me = this;
83 if (this.timer !== null) clearTimeout(this.timer);
84 this.timer = setTimeout(function () { me.func(); }, 50);
85 }
86};
6cfd4191 87DEP.Soon = Soon;
ac26861c
MW
88
89/*----- Conversion machinery ----------------------------------------------*/
90
91/* An exception, thrown if a conversion function doesn't like what it
92 * sees.
93 */
6cfd4191 94BadValue = new DEP.Tag('BadValue'); DEP.BadValue = BadValue;
ac26861c 95
6cfd4191 96function convert_to_numeric(string) {
ac26861c
MW
97 /* Convert the argument STRING to a number. */
98
99 if (!string.match('\\S')) throw BadValue;
100 var n = Number(string);
101 if (n !== n) throw BadValue;
102 return n;
103}
6cfd4191 104DEP_UI.convert_to_numeric = convert_to_numeric;
ac26861c 105
6cfd4191 106function convert_from_numeric(num) {
ac26861c
MW
107 /* Convert the argument number NUM to a string, in a useful way. */
108 return num.toFixed(3);
109}
6cfd4191 110DEP_UI.convert_from_numeric = convert_from_numeric;
ac26861c
MW
111
112/*----- User interface functions ------------------------------------------*/
113
114/* A list of input fields which might need periodic kicking. */
115var KICK_INPUT_FIELDS = [];
116
6132735f
MW
117function input_widget(id, dep, kickfn) {
118 /* Bind an input widget (with the given ID) to a DEP, calling KICKFN (with
119 * the widget element as its argument) to update DEP from the state of the
120 * widget. This is common machinery for `input_field' and `input_radio'.
121 */
122
123 var e = elt(id);
124
125 // Name the dep after our id.
126 dep.name = id;
127
128 // Arrange to update the dep `shortly after' updates.
129 function kick() { kickfn(e); }
130 var soon = new Soon(kick);
131 function kick_soon() { soon.kick(); }
132 e.addEventListener('input', kick_soon, false);
133
134 // Set our field to the correct state when the page finishes loading.
135 KICK_INPUT_FIELDS.push(kick);
136}
137
6cfd4191 138function input_field(id, dep, convert) {
ac26861c
MW
139 /* Bind an input field (with the given ID) to a DEP, converting the user's
140 * input with the CONVERT function.
141 */
142
6132735f
MW
143 input_widget(id, dep, function (e) {
144 /* Update the dep from the element content. If the CONVERT function
ac26861c
MW
145 * doesn't like the input then mark the dep as bad and highlight the
146 * input element.
147 */
148
149 var val, err;
150
151 try {
152 val = convert(e.value);
153 if (!dep.goodp())
154 rm_elt_class(e, 'bad');
155 dep.set_value(val);
156 } catch (err) {
157 if (err !== BadValue) throw err;
158 dep.make_bad();
159 add_elt_class(e, 'bad');
160 }
6132735f 161 });
ac26861c 162}
6cfd4191 163DEP_UI.input_field = input_field;
ac26861c 164
6cfd4191 165function input_radio(id, dep) {
ac26861c
MW
166 /* Bind a radio button (with the given ID) to a DEP. When the user frobs
167 * the button, set the dep to the element's `value' attribute.
168 */
169
6132735f
MW
170 input_widget(id, dep, function (e) {
171 // Make sure we're actually chosen. We might get called regardless of
172 // user input.
ac26861c 173 if (e.checked) dep.set_value(e.value);
6132735f 174 });
ac26861c 175}
6cfd4191 176DEP_UI.input_radio = input_radio;
ac26861c 177
6cfd4191 178function output_field(id, dep, convert) {
ac26861c
MW
179 /* Bind a DEP to an output element (given by ID), converting the dep's
180 * value using the CONVERT function.
181 */
182
183 var e = elt(id);
184
185 function kicked() {
186 /* Update the element, highlighting it if the dep is bad. */
187 if (dep.goodp()) {
188 rm_elt_class(e, 'bad');
189 e.value = convert(dep.value());
190 } else {
191 add_elt_class(e, 'bad');
192 e.value = '';
193 }
194 }
195
196 // Name the dep after our id.
197 dep.name = id;
198
199 // Keep track of the dep's value.
200 dep.add_listener(kicked);
201 kicked();
202}
6cfd4191 203DEP_UI.output_field = output_field;
ac26861c
MW
204
205/*----- Periodic maintenance ----------------------------------------------*/
206
207function kick_all() {
208 /* Kick all of the input fields we know about. Their `kick' functions are
209 * all on the list `KICK_INPUT_FIELDS'.
210 */
211 DEP.dolist(KICK_INPUT_FIELDS, function (func) { func(); });
212}
213
ac26861c 214// And make sure we get everything started when the page is fully loaded.
f046c40d 215window.addEventListener('load', kick_all, false);
ac26861c
MW
216
217/*----- That's all, folks -------------------------------------------------*/
6cfd4191 218})();