chiark / gitweb /
dep-ui.js: Add some debugging machinery.
[dep-ui] / dep-ui.js
CommitLineData
ac26861c
MW
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
24var DEP_UI = {}; (function () { with (DEP_UI) {
25
26/*----- Utility functions and classes -------------------------------------*/
27
c6150f2f
MW
28DEP_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
ac26861c
MW
44DEP_UI.elt = function (id) {
45 /* Find and return the element with the given ID. */
46 return document.getElementById(id);
47}
48
49DEP_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
56DEP_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 */
66DEP_UI.Soon = function (func) {
67 this.timer = null;
68 this.func = func;
69}
70Soon.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 */
88DEP_UI.BadValue = new DEP.Tag('BadValue');
89
90DEP_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
99DEP_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. */
107var KICK_INPUT_FIELDS = [];
108
109DEP_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);
143 e.addEventListener('blur', kick_soon);
144 e.addEventListener('keypress', kick_soon);
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
153DEP_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);
173 e.addEventListener('changed', kick_soon);
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
180DEP_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
208function 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.
216setInterval(kick_all, 500);
217
218// And make sure we get everything started when the page is fully loaded.
219window.addEventListener('load', kick_all);
220
221/*----- That's all, folks -------------------------------------------------*/
222} })();