8e8e7674f4806aef77fd2543fa2cbc47be8d54ff
3 * Dependency-based user interface in a web page.
5 * (c) 2013 Mark Wooding
8 /*----- Licensing notice --------------------------------------------------*
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.
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.
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/>.
24 var DEP_UI
= {}; (function () {
26 /*----- Utility functions and classes -------------------------------------*/
29 /* Write the string MSG to the `trace' element, if there is one. */
32 if (e
!== null) e
.textContent
+= msg
;
36 function trap(what
, func
) {
40 debug('caught exception in ' + what
+ ': ' + e
);
47 /* Find and return the element with the given ID. */
48 return document
.getElementById(id
);
52 function add_elt_class(elt
, cls
) {
53 /* Add the class name CLS to element ELT's `class' attribute. */
55 if (!elt
.className
.match('\\b' + cls
+ '\\b'))
56 elt
.className
+= ' ' + cls
58 DEP_UI
.add_elt_class
= add_elt_class
;
60 function rm_elt_class(elt
, cls
) {
61 /* Remove the class name CLS from element ELT's `class' attribute. */
63 elt
.className
= elt
.className
.replace(
64 new RegExp ('\\s*\\b' + cls
+ '\\b\\s*'), ' ');
66 DEP_UI
.rm_elt_class
= rm_elt_class
;
68 /* A gadget which can arrange to perform an idempotent action (the FUNC
69 * argument) again `soon'.
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
83 if (this.timer
!== null) clearTimeout(this.timer
);
84 this.timer
= setTimeout(function () { me
.func(); }, 50);
89 /*----- Conversion machinery ----------------------------------------------*/
91 /* An exception, thrown if a conversion function doesn't like what it
94 BadValue
= new DEP
.Tag('BadValue'); DEP
.BadValue
= BadValue
;
96 function convert_to_numeric(string
) {
97 /* Convert the argument STRING to a number. */
99 if (!string
.match('\\S')) throw BadValue
;
100 var n
= Number(string
);
101 if (n
!== n
) throw BadValue
;
104 DEP_UI
.convert_to_numeric
= convert_to_numeric
;
106 function convert_from_numeric(num
) {
107 /* Convert the argument number NUM to a string, in a useful way. */
108 return num
.toFixed(3);
110 DEP_UI
.convert_from_numeric
= convert_from_numeric
;
112 /*----- User interface functions ------------------------------------------*/
114 /* A list of input fields which might need periodic kicking. */
115 var KICK_INPUT_FIELDS
= [];
117 function input_field(id
, dep
, convert
) {
118 /* Bind an input field (with the given ID) to a DEP, converting the user's
119 * input with the CONVERT function.
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
133 val
= convert(e
.value
);
135 rm_elt_class(e
, 'bad');
138 if (err
!== BadValue
) throw err
;
140 add_elt_class(e
, 'bad');
144 // Name the dep after our id.
147 // Arrange to update the dep `shortly after' updates.
148 var soon
= new Soon(kick
);
149 function kick_soon() { soon
.kick(); }
150 e
.addEventListener('click', kick_soon
, false);
151 e
.addEventListener('blur', kick_soon
, false);
152 e
.addEventListener('keypress', kick_soon
, false);
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
);
160 DEP_UI
.input_field
= input_field
;
162 function input_radio(id
, dep
) {
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.
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
);
175 // Name the dep after our id.
178 // Arrange to update the dep `shortly after' updates.
179 var soon
= new Soon(kick
);
180 function kick_soon() { soon
.kick(); }
181 e
.addEventListener('click', kick_soon
, false);
182 e
.addEventListener('changed', kick_soon
, false);
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
);
188 DEP_UI
.input_radio
= input_radio
;
190 function output_field(id
, dep
, convert
) {
191 /* Bind a DEP to an output element (given by ID), converting the dep's
192 * value using the CONVERT function.
198 /* Update the element, highlighting it if the dep is bad. */
200 rm_elt_class(e
, 'bad');
201 e
.value
= convert(dep
.value());
203 add_elt_class(e
, 'bad');
208 // Name the dep after our id.
211 // Keep track of the dep's value.
212 dep
.add_listener(kicked
);
215 DEP_UI
.output_field
= output_field
;
217 /*----- Periodic maintenance ----------------------------------------------*/
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'.
223 DEP
.dolist(KICK_INPUT_FIELDS
, function (func
) { func(); });
226 // Update the input fields relatively frequently.
227 setInterval(kick_all
, 500);
229 // And make sure we get everything started when the page is fully loaded.
230 window
.addEventListener('load', kick_all
, false);
232 /*----- That's all, folks -------------------------------------------------*/