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 | ||
6cfd4191 | 117 | function input_field(id, dep, convert) { |
ac26861c MW |
118 | /* Bind an input field (with the given ID) to a DEP, converting the user's |
119 | * input with the CONVERT function. | |
120 | */ | |
121 | ||
122 | var e = elt(id); | |
123 | ||
124 | function kick() { | |
125 | /* Update the dep from the element content. If the convert function | |
126 | * doesn't like the input then mark the dep as bad and highlight the | |
127 | * input element. | |
128 | */ | |
129 | ||
130 | var val, err; | |
131 | ||
132 | try { | |
133 | val = convert(e.value); | |
134 | if (!dep.goodp()) | |
135 | rm_elt_class(e, 'bad'); | |
136 | dep.set_value(val); | |
137 | } catch (err) { | |
138 | if (err !== BadValue) throw err; | |
139 | dep.make_bad(); | |
140 | add_elt_class(e, 'bad'); | |
141 | } | |
142 | } | |
143 | ||
144 | // Name the dep after our id. | |
145 | dep.name = id; | |
146 | ||
147 | // Arrange to update the dep `shortly after' updates. | |
148 | var soon = new Soon(kick); | |
6cfd4191 | 149 | function kick_soon() { soon.kick(); } |
f046c40d MW |
150 | e.addEventListener('click', kick_soon, false); |
151 | e.addEventListener('blur', kick_soon, false); | |
152 | e.addEventListener('keypress', kick_soon, false); | |
ac26861c MW |
153 | |
154 | // Sadly, the collection of events above isn't comprehensive, because we | |
155 | // don't actually get told about edits as a result of clipboard operations, | |
156 | // or even (sometimes) deletes, so add our `kick' function to a list of | |
157 | // such functions to be run periodically just in case. | |
158 | KICK_INPUT_FIELDS.push(kick); | |
159 | } | |
6cfd4191 | 160 | DEP_UI.input_field = input_field; |
ac26861c | 161 | |
6cfd4191 | 162 | function input_radio(id, dep) { |
ac26861c MW |
163 | /* Bind a radio button (with the given ID) to a DEP. When the user frobs |
164 | * the button, set the dep to the element's `value' attribute. | |
165 | */ | |
166 | ||
167 | var e = elt(id); | |
168 | ||
6cfd4191 | 169 | function kick() { |
ac26861c MW |
170 | // Make sure we're actually chosen. We get called periodically |
171 | // regardless of user input. | |
172 | if (e.checked) dep.set_value(e.value); | |
173 | }; | |
174 | ||
175 | // Name the dep after our id. | |
176 | dep.name = id; | |
177 | ||
178 | // Arrange to update the dep `shortly after' updates. | |
179 | var soon = new Soon(kick); | |
6cfd4191 | 180 | function kick_soon() { soon.kick(); } |
f046c40d MW |
181 | e.addEventListener('click', kick_soon, false); |
182 | e.addEventListener('changed', kick_soon, false); | |
ac26861c MW |
183 | |
184 | // The situation for radio buttons doesn't seem as bad as for text widgets, | |
185 | // but let's be on the safe side. | |
186 | KICK_INPUT_FIELDS.push(kick); | |
187 | } | |
6cfd4191 | 188 | DEP_UI.input_radio = input_radio; |
ac26861c | 189 | |
6cfd4191 | 190 | function output_field(id, dep, convert) { |
ac26861c MW |
191 | /* Bind a DEP to an output element (given by ID), converting the dep's |
192 | * value using the CONVERT function. | |
193 | */ | |
194 | ||
195 | var e = elt(id); | |
196 | ||
197 | function kicked() { | |
198 | /* Update the element, highlighting it if the dep is bad. */ | |
199 | if (dep.goodp()) { | |
200 | rm_elt_class(e, 'bad'); | |
201 | e.value = convert(dep.value()); | |
202 | } else { | |
203 | add_elt_class(e, 'bad'); | |
204 | e.value = ''; | |
205 | } | |
206 | } | |
207 | ||
208 | // Name the dep after our id. | |
209 | dep.name = id; | |
210 | ||
211 | // Keep track of the dep's value. | |
212 | dep.add_listener(kicked); | |
213 | kicked(); | |
214 | } | |
6cfd4191 | 215 | DEP_UI.output_field = output_field; |
ac26861c MW |
216 | |
217 | /*----- Periodic maintenance ----------------------------------------------*/ | |
218 | ||
219 | function kick_all() { | |
220 | /* Kick all of the input fields we know about. Their `kick' functions are | |
221 | * all on the list `KICK_INPUT_FIELDS'. | |
222 | */ | |
223 | DEP.dolist(KICK_INPUT_FIELDS, function (func) { func(); }); | |
224 | } | |
225 | ||
226 | // Update the input fields relatively frequently. | |
227 | setInterval(kick_all, 500); | |
228 | ||
229 | // And make sure we get everything started when the page is fully loaded. | |
f046c40d | 230 | window.addEventListener('load', kick_all, false); |
ac26861c MW |
231 | |
232 | /*----- That's all, folks -------------------------------------------------*/ | |
6cfd4191 | 233 | })(); |