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 () { with (DEP_UI
) {
26 /*----- Utility functions and classes -------------------------------------*/
28 DEP_UI
.elt
= function (id
) {
29 /* Find and return the element with the given ID. */
30 return document
.getElementById(id
);
33 DEP_UI
.add_elt_class
= function (elt
, cls
) {
34 /* Add the class name CLS to element ELT's `class' attribute. */
36 if (!elt
.className
.match('\\b' + cls
+ '\\b'))
37 elt
.className
+= ' ' + cls
40 DEP_UI
.rm_elt_class
= function (elt
, cls
) {
41 /* Remove the class name CLS from element ELT's `class' attribute. */
43 elt
.className
= elt
.className
.replace(
44 new RegExp ('\\s*\\b' + cls
+ '\\b\\s*'), ' ');
47 /* A gadget which can arrange to perform an idempotent action (the FUNC
48 * argument) again `soon'.
50 DEP_UI
.Soon
= function (func
) {
56 /* Make sure the function is called again soon. If we've already been
57 * kicked, then put off the action for a bit more (in case things need
62 if (this.timer
!== null) clearTimeout(this.timer
);
63 this.timer
= setTimeout(function () { me
.func(); }, 50);
67 /*----- Conversion machinery ----------------------------------------------*/
69 /* An exception, thrown if a conversion function doesn't like what it
72 DEP_UI
.BadValue
= new DEP
.Tag('BadValue');
74 DEP_UI
.convert_to_numeric
= function (string
) {
75 /* Convert the argument STRING to a number. */
77 if (!string
.match('\\S')) throw BadValue
;
78 var n
= Number(string
);
79 if (n
!== n
) throw BadValue
;
83 DEP_UI
.convert_from_numeric
= function (num
) {
84 /* Convert the argument number NUM to a string, in a useful way. */
85 return num
.toFixed(3);
88 /*----- User interface functions ------------------------------------------*/
90 /* A list of input fields which might need periodic kicking. */
91 var KICK_INPUT_FIELDS
= [];
93 DEP_UI
.input_field
= function (id
, dep
, convert
) {
94 /* Bind an input field (with the given ID) to a DEP, converting the user's
95 * input with the CONVERT function.
101 /* Update the dep from the element content. If the convert function
102 * doesn't like the input then mark the dep as bad and highlight the
109 val
= convert(e
.value
);
111 rm_elt_class(e
, 'bad');
114 if (err
!== BadValue
) throw err
;
116 add_elt_class(e
, 'bad');
120 // Name the dep after our id.
123 // Arrange to update the dep `shortly after' updates.
124 var soon
= new Soon(kick
);
125 function kick_soon () { soon
.kick(); }
126 e
.addEventListener('click', kick_soon
);
127 e
.addEventListener('blur', kick_soon
);
128 e
.addEventListener('keypress', kick_soon
);
130 // Sadly, the collection of events above isn't comprehensive, because we
131 // don't actually get told about edits as a result of clipboard operations,
132 // or even (sometimes) deletes, so add our `kick' function to a list of
133 // such functions to be run periodically just in case.
134 KICK_INPUT_FIELDS
.push(kick
);
137 DEP_UI
.input_radio
= function (id
, dep
) {
138 /* Bind a radio button (with the given ID) to a DEP. When the user frobs
139 * the button, set the dep to the element's `value' attribute.
145 // Make sure we're actually chosen. We get called periodically
146 // regardless of user input.
147 if (e
.checked
) dep
.set_value(e
.value
);
150 // Name the dep after our id.
153 // Arrange to update the dep `shortly after' updates.
154 var soon
= new Soon(kick
);
155 function kick_soon () { soon
.kick(); }
156 e
.addEventListener('click', kick_soon
);
157 e
.addEventListener('changed', kick_soon
);
159 // The situation for radio buttons doesn't seem as bad as for text widgets,
160 // but let's be on the safe side.
161 KICK_INPUT_FIELDS
.push(kick
);
164 DEP_UI
.output_field
= function (id
, dep
, convert
) {
165 /* Bind a DEP to an output element (given by ID), converting the dep's
166 * value using the CONVERT function.
172 /* Update the element, highlighting it if the dep is bad. */
174 rm_elt_class(e
, 'bad');
175 e
.value
= convert(dep
.value());
177 add_elt_class(e
, 'bad');
182 // Name the dep after our id.
185 // Keep track of the dep's value.
186 dep
.add_listener(kicked
);
190 /*----- Periodic maintenance ----------------------------------------------*/
192 function kick_all() {
193 /* Kick all of the input fields we know about. Their `kick' functions are
194 * all on the list `KICK_INPUT_FIELDS'.
196 DEP
.dolist(KICK_INPUT_FIELDS
, function (func
) { func(); });
199 // Update the input fields relatively frequently.
200 setInterval(kick_all
, 500);
202 // And make sure we get everything started when the page is fully loaded.
203 window
.addEventListener('load', kick_all
);
205 /*----- That's all, folks -------------------------------------------------*/