Commit | Line | Data |
---|---|---|
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 | 24 | var DEP_UI = {}; (function () { |
ac26861c MW |
25 | |
26 | /*----- Utility functions and classes -------------------------------------*/ | |
27 | ||
6cfd4191 | 28 | function 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 | 34 | DEP_UI.debug = debug; |
c6150f2f | 35 | |
6cfd4191 | 36 | function 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 | 44 | DEP_UI.trap = trap; |
c6150f2f | 45 | |
6cfd4191 | 46 | function elt(id) { |
ac26861c MW |
47 | /* Find and return the element with the given ID. */ |
48 | return document.getElementById(id); | |
49 | } | |
6cfd4191 | 50 | DEP_UI.elt = elt; |
ac26861c | 51 | |
6cfd4191 | 52 | function 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 | 58 | DEP_UI.add_elt_class = add_elt_class; |
ac26861c | 59 | |
6cfd4191 | 60 | function 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 | 66 | DEP_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 | 71 | function Soon(func) { |
ac26861c MW |
72 | this.timer = null; |
73 | this.func = func; | |
74 | } | |
75 | Soon.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 | 87 | DEP.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 | 94 | BadValue = new DEP.Tag('BadValue'); DEP.BadValue = BadValue; |
ac26861c | 95 | |
6cfd4191 | 96 | function 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 | 104 | DEP_UI.convert_to_numeric = convert_to_numeric; |
ac26861c | 105 | |
6cfd4191 | 106 | function 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 | 110 | DEP_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. */ | |
115 | var KICK_INPUT_FIELDS = []; | |
116 | ||
6132735f MW |
117 | function 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 | 138 | function 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 | 163 | DEP_UI.input_field = input_field; |
ac26861c | 164 | |
6cfd4191 | 165 | function 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 | 176 | DEP_UI.input_radio = input_radio; |
ac26861c | 177 | |
6cfd4191 | 178 | function 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 | 203 | DEP_UI.output_field = output_field; |
ac26861c MW |
204 | |
205 | /*----- Periodic maintenance ----------------------------------------------*/ | |
206 | ||
207 | function 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 | 215 | window.addEventListener('load', kick_all, false); |
ac26861c MW |
216 | |
217 | /*----- That's all, folks -------------------------------------------------*/ | |
6cfd4191 | 218 | })(); |