chiark / gitweb /
rolling.html, rolling-eqn.html: Describe the calculation.
[dep-ui] / dep-ui.js
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
28 DEP_UI.elt = function (id) {
29   /* Find and return the element with the given ID. */
30   return document.getElementById(id);
31 }
32
33 DEP_UI.add_elt_class = function (elt, cls) {
34   /* Add the class name CLS to element ELT's `class' attribute. */
35
36   if (!elt.className.match('\\b' + cls + '\\b'))
37     elt.className += ' ' + cls
38 }
39
40 DEP_UI.rm_elt_class = function (elt, cls) {
41   /* Remove the class name CLS from element ELT's `class' attribute. */
42
43   elt.className = elt.className.replace(
44     new RegExp ('\\s*\\b' + cls + '\\b\\s*'), ' ');
45 }
46
47 /* A gadget which can arrange to perform an idempotent action (the FUNC
48  * argument) again `soon'.
49  */
50 DEP_UI.Soon = function (func) {
51   this.timer = null;
52   this.func = func;
53 }
54 Soon.prototype = {
55   kick: function () {
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
58      * time to settle).
59      */
60
61     var me = this;
62     if (this.timer !== null) clearTimeout(this.timer);
63     this.timer = setTimeout(function () { me.func(); }, 50);
64   }
65 };
66
67 /*----- Conversion machinery ----------------------------------------------*/
68
69 /* An exception, thrown if a conversion function doesn't like what it
70  * sees.
71  */
72 DEP_UI.BadValue = new DEP.Tag('BadValue');
73
74 DEP_UI.convert_to_numeric = function (string) {
75   /* Convert the argument STRING to a number. */
76
77   if (!string.match('\\S')) throw BadValue;
78   var n = Number(string);
79   if (n !== n) throw BadValue;
80   return n;
81 }
82
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);
86 }
87
88 /*----- User interface functions ------------------------------------------*/
89
90 /* A list of input fields which might need periodic kicking. */
91 var KICK_INPUT_FIELDS = [];
92
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.
96    */
97
98   var e = elt(id);
99
100   function kick() {
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
103      * input element.
104      */
105
106     var val, err;
107
108     try {
109       val = convert(e.value);
110       if (!dep.goodp())
111         rm_elt_class(e, 'bad');
112       dep.set_value(val);
113     } catch (err) {
114       if (err !== BadValue) throw err;
115       dep.make_bad();
116       add_elt_class(e, 'bad');
117     }
118   }
119
120   // Name the dep after our id.
121   dep.name = id;
122
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);
129
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);
135 }
136
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.
140    */
141
142   var e = elt(id);
143
144   function kick () {
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);
148   };
149
150   // Name the dep after our id.
151   dep.name = id;
152
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);
158
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);
162 }
163
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.
167    */
168
169   var e = elt(id);
170
171   function kicked() {
172     /* Update the element, highlighting it if the dep is bad. */
173     if (dep.goodp()) {
174       rm_elt_class(e, 'bad');
175       e.value = convert(dep.value());
176     } else {
177       add_elt_class(e, 'bad');
178       e.value = '';
179     }
180   }
181
182   // Name the dep after our id.
183   dep.name = id;
184
185   // Keep track of the dep's value.
186   dep.add_listener(kicked);
187   kicked();
188 }
189
190 /*----- Periodic maintenance ----------------------------------------------*/
191
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'.
195    */
196   DEP.dolist(KICK_INPUT_FIELDS, function (func) { func(); });
197 }
198
199 // Update the input fields relatively frequently.
200 setInterval(kick_all, 500);
201
202 // And make sure we get everything started when the page is fully loaded.
203 window.addEventListener('load', kick_all);
204
205 /*----- That's all, folks -------------------------------------------------*/
206 } })();