chiark / gitweb /
rolling.html: Fix apostrophe, for consistency's sake.
[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
28DEP_UI.elt = function (id) {
29 /* Find and return the element with the given ID. */
30 return document.getElementById(id);
31}
32
33DEP_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
40DEP_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 */
50DEP_UI.Soon = function (func) {
51 this.timer = null;
52 this.func = func;
53}
54Soon.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 */
72DEP_UI.BadValue = new DEP.Tag('BadValue');
73
74DEP_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
83DEP_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. */
91var KICK_INPUT_FIELDS = [];
92
93DEP_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
137DEP_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
164DEP_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
192function 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.
200setInterval(kick_all, 500);
201
202// And make sure we get everything started when the page is fully loaded.
203window.addEventListener('load', kick_all);
204
205/*----- That's all, folks -------------------------------------------------*/
206} })();