chiark / gitweb /
rolling.html, rolling-eqn.html: Hack for mobile browsers.
[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.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
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, false);
143   e.addEventListener('blur', kick_soon, false);
144   e.addEventListener('keypress', kick_soon, false);
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, false);
173   e.addEventListener('changed', kick_soon, false);
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, false);
220
221 /*----- That's all, folks -------------------------------------------------*/
222 } })();