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 | |
21 | * along with this program; if not, see <http://www.gnu.org/licenses/>. | |
22 | */ | |
23 | ||
24 | var DEP_UI = {}; (function () { with (DEP_UI) { | |
25 | ||
26 | /*----- Utility functions and classes -------------------------------------*/ | |
27 | ||
c6150f2f MW |
28 | DEP_UI.debug = function(msg) { |
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 | } | |
34 | ||
35 | DEP_UI.trap = function(what, func) { | |
36 | try { | |
37 | func(); | |
38 | } catch (e) { | |
39 | debug('caught exception in ' + what + ': ' + e); | |
40 | throw e; | |
41 | } | |
42 | } | |
43 | ||
ac26861c MW |
44 | DEP_UI.elt = function (id) { |
45 | /* Find and return the element with the given ID. */ | |
46 | return document.getElementById(id); | |
47 | } | |
48 | ||
49 | DEP_UI.add_elt_class = function (elt, cls) { | |
50 | /* Add the class name CLS to element ELT's `class' attribute. */ | |
51 | ||
52 | if (!elt.className.match('\\b' + cls + '\\b')) | |
53 | elt.className += ' ' + cls | |
54 | } | |
55 | ||
56 | DEP_UI.rm_elt_class = function (elt, cls) { | |
57 | /* Remove the class name CLS from element ELT's `class' attribute. */ | |
58 | ||
59 | elt.className = elt.className.replace( | |
60 | new RegExp ('\\s*\\b' + cls + '\\b\\s*'), ' '); | |
61 | } | |
62 | ||
63 | /* A gadget which can arrange to perform an idempotent action (the FUNC | |
64 | * argument) again `soon'. | |
65 | */ | |
66 | DEP_UI.Soon = function (func) { | |
67 | this.timer = null; | |
68 | this.func = func; | |
69 | } | |
70 | Soon.prototype = { | |
71 | kick: function () { | |
72 | /* Make sure the function is called again soon. If we've already been | |
73 | * kicked, then put off the action for a bit more (in case things need | |
74 | * time to settle). | |
75 | */ | |
76 | ||
77 | var me = this; | |
78 | if (this.timer !== null) clearTimeout(this.timer); | |
79 | this.timer = setTimeout(function () { me.func(); }, 50); | |
80 | } | |
81 | }; | |
82 | ||
83 | /*----- Conversion machinery ----------------------------------------------*/ | |
84 | ||
85 | /* An exception, thrown if a conversion function doesn't like what it | |
86 | * sees. | |
87 | */ | |
88 | DEP_UI.BadValue = new DEP.Tag('BadValue'); | |
89 | ||
90 | DEP_UI.convert_to_numeric = function (string) { | |
91 | /* Convert the argument STRING to a number. */ | |
92 | ||
93 | if (!string.match('\\S')) throw BadValue; | |
94 | var n = Number(string); | |
95 | if (n !== n) throw BadValue; | |
96 | return n; | |
97 | } | |
98 | ||
99 | DEP_UI.convert_from_numeric = function (num) { | |
100 | /* Convert the argument number NUM to a string, in a useful way. */ | |
101 | return num.toFixed(3); | |
102 | } | |
103 | ||
104 | /*----- User interface functions ------------------------------------------*/ | |
105 | ||
106 | /* A list of input fields which might need periodic kicking. */ | |
107 | var KICK_INPUT_FIELDS = []; | |
108 | ||
109 | DEP_UI.input_field = function (id, dep, convert) { | |
110 | /* Bind an input field (with the given ID) to a DEP, converting the user's | |
111 | * input with the CONVERT function. | |
112 | */ | |
113 | ||
114 | var e = elt(id); | |
115 | ||
116 | function kick() { | |
117 | /* Update the dep from the element content. If the convert function | |
118 | * doesn't like the input then mark the dep as bad and highlight the | |
119 | * input element. | |
120 | */ | |
121 | ||
122 | var val, err; | |
123 | ||
124 | try { | |
125 | val = convert(e.value); | |
126 | if (!dep.goodp()) | |
127 | rm_elt_class(e, 'bad'); | |
128 | dep.set_value(val); | |
129 | } catch (err) { | |
130 | if (err !== BadValue) throw err; | |
131 | dep.make_bad(); | |
132 | add_elt_class(e, 'bad'); | |
133 | } | |
134 | } | |
135 | ||
136 | // Name the dep after our id. | |
137 | dep.name = id; | |
138 | ||
139 | // Arrange to update the dep `shortly after' updates. | |
140 | var soon = new Soon(kick); | |
141 | function kick_soon () { soon.kick(); } | |
142 | e.addEventListener('click', kick_soon); | |
143 | e.addEventListener('blur', kick_soon); | |
144 | e.addEventListener('keypress', kick_soon); | |
145 | ||
146 | // Sadly, the collection of events above isn't comprehensive, because we | |
147 | // don't actually get told about edits as a result of clipboard operations, | |
148 | // or even (sometimes) deletes, so add our `kick' function to a list of | |
149 | // such functions to be run periodically just in case. | |
150 | KICK_INPUT_FIELDS.push(kick); | |
151 | } | |
152 | ||
153 | DEP_UI.input_radio = function (id, dep) { | |
154 | /* Bind a radio button (with the given ID) to a DEP. When the user frobs | |
155 | * the button, set the dep to the element's `value' attribute. | |
156 | */ | |
157 | ||
158 | var e = elt(id); | |
159 | ||
160 | function kick () { | |
161 | // Make sure we're actually chosen. We get called periodically | |
162 | // regardless of user input. | |
163 | if (e.checked) dep.set_value(e.value); | |
164 | }; | |
165 | ||
166 | // Name the dep after our id. | |
167 | dep.name = id; | |
168 | ||
169 | // Arrange to update the dep `shortly after' updates. | |
170 | var soon = new Soon(kick); | |
171 | function kick_soon () { soon.kick(); } | |
172 | e.addEventListener('click', kick_soon); | |
173 | e.addEventListener('changed', kick_soon); | |
174 | ||
175 | // The situation for radio buttons doesn't seem as bad as for text widgets, | |
176 | // but let's be on the safe side. | |
177 | KICK_INPUT_FIELDS.push(kick); | |
178 | } | |
179 | ||
180 | DEP_UI.output_field = function (id, dep, convert) { | |
181 | /* Bind a DEP to an output element (given by ID), converting the dep's | |
182 | * value using the CONVERT function. | |
183 | */ | |
184 | ||
185 | var e = elt(id); | |
186 | ||
187 | function kicked() { | |
188 | /* Update the element, highlighting it if the dep is bad. */ | |
189 | if (dep.goodp()) { | |
190 | rm_elt_class(e, 'bad'); | |
191 | e.value = convert(dep.value()); | |
192 | } else { | |
193 | add_elt_class(e, 'bad'); | |
194 | e.value = ''; | |
195 | } | |
196 | } | |
197 | ||
198 | // Name the dep after our id. | |
199 | dep.name = id; | |
200 | ||
201 | // Keep track of the dep's value. | |
202 | dep.add_listener(kicked); | |
203 | kicked(); | |
204 | } | |
205 | ||
206 | /*----- Periodic maintenance ----------------------------------------------*/ | |
207 | ||
208 | function kick_all() { | |
209 | /* Kick all of the input fields we know about. Their `kick' functions are | |
210 | * all on the list `KICK_INPUT_FIELDS'. | |
211 | */ | |
212 | DEP.dolist(KICK_INPUT_FIELDS, function (func) { func(); }); | |
213 | } | |
214 | ||
215 | // Update the input fields relatively frequently. | |
216 | setInterval(kick_all, 500); | |
217 | ||
218 | // And make sure we get everything started when the page is fully loaded. | |
219 | window.addEventListener('load', kick_all); | |
220 | ||
221 | /*----- That's all, folks -------------------------------------------------*/ | |
222 | } })(); |