chiark / gitweb /
dep.js: Fix the way `STATE' is handled.
[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 <https://www.gnu.org/licenses/>.
22  */
23
24 var DEP_UI = {}; (function () {
25
26 /*----- Utility functions and classes -------------------------------------*/
27
28 function debug(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 DEP_UI.debug = debug;
35
36 function trap(what, func) {
37   try {
38     func();
39   } catch (e) {
40     debug('caught exception in ' + what + ': ' + e);
41     throw e;
42   }
43 }
44 DEP_UI.trap = trap;
45
46 function elt(id) {
47   /* Find and return the element with the given ID. */
48   return document.getElementById(id);
49 }
50 DEP_UI.elt = elt;
51
52 function add_elt_class(elt, cls) {
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 }
58 DEP_UI.add_elt_class = add_elt_class;
59
60 function rm_elt_class(elt, cls) {
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 }
66 DEP_UI.rm_elt_class = rm_elt_class;
67
68 /* A gadget which can arrange to perform an idempotent action (the FUNC
69  * argument) again `soon'.
70  */
71 function Soon(func) {
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 };
87 DEP.Soon = Soon;
88
89 /*----- Conversion machinery ----------------------------------------------*/
90
91 /* An exception, thrown if a conversion function doesn't like what it
92  * sees.
93  */
94 BadValue = new DEP.Tag('BadValue'); DEP.BadValue = BadValue;
95
96 function convert_to_numeric(string) {
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 }
104 DEP_UI.convert_to_numeric = convert_to_numeric;
105
106 function convert_from_numeric(num) {
107   /* Convert the argument number NUM to a string, in a useful way. */
108   return num.toFixed(3);
109 }
110 DEP_UI.convert_from_numeric = convert_from_numeric;
111
112 /*----- User interface functions ------------------------------------------*/
113
114 /* A list of input fields which might need periodic kicking. */
115 var KICK_INPUT_FIELDS = [];
116
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.
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);
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);
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 }
160 DEP_UI.input_field = input_field;
161
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.
165    */
166
167   var e = elt(id);
168
169   function kick() {
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);
180   function kick_soon() { soon.kick(); }
181   e.addEventListener('click', kick_soon, false);
182   e.addEventListener('changed', kick_soon, false);
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 }
188 DEP_UI.input_radio = input_radio;
189
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.
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 }
215 DEP_UI.output_field = output_field;
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.
230 window.addEventListener('load', kick_all, false);
231
232 /*----- That's all, folks -------------------------------------------------*/
233 })();