Commit | Line | Data |
---|---|---|
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 | ||
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 | } })(); |