Merge lp:~methanal-developers/methanal/form-row-striping into lp:methanal
- form-row-striping
- Merge into trunk
Proposed by
Jonathan Jacobs
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 140 | ||||
Proposed branch: | lp:~methanal-developers/methanal/form-row-striping | ||||
Merge into: | lp:methanal | ||||
Diff against target: |
464 lines (+251/-21) 6 files modified
methanal/js/Methanal/Tests/TestUtil.js (+77/-0) methanal/js/Methanal/Tests/TestView.js (+3/-3) methanal/js/Methanal/Util.js (+100/-7) methanal/js/Methanal/View.js (+63/-7) methanal/static/styles/methanal.css (+6/-2) methanal/view.py (+2/-2) |
||||
To merge this branch: | bzr merge lp:~methanal-developers/methanal/form-row-striping | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tristan Seligmann | Needs Fixing | ||
Review via email: mp+29297@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Tristan Seligmann (mithrandi) : | # |
review:
Approve
Revision history for this message
Jonathan Jacobs (jjacobs) wrote : | # |
Revision history for this message
Tristan Seligmann (mithrandi) wrote : | # |
148 + throw new Error('"pred" must be a function or null');
This should read "a function, null, or undefined".
review:
Needs Fixing
- 144. By Jonathan Jacobs
-
Improve exception description.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'methanal/js/Methanal/Tests/TestUtil.js' |
2 | --- methanal/js/Methanal/Tests/TestUtil.js 2010-02-23 10:41:06 +0000 |
3 | +++ methanal/js/Methanal/Tests/TestUtil.js 2010-07-08 18:27:43 +0000 |
4 | @@ -290,6 +290,83 @@ |
5 | self.assertArraysEqual( |
6 | Methanal.Util.divmod(23, 12), |
7 | [1, 11]); |
8 | + }, |
9 | + |
10 | + |
11 | + /** |
12 | + * L{Methanal.Util.nthItem} applies a function to the nth item in an |
13 | + * C{Array}. |
14 | + */ |
15 | + function test_nthItem(self) { |
16 | + var seq = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; |
17 | + |
18 | + function gatherNth(start, n) { |
19 | + var res = [] |
20 | + Methanal.Util.nthItem(seq, start, n, function (v) { |
21 | + res.push(v); |
22 | + }); |
23 | + return res; |
24 | + } |
25 | + |
26 | + self.assertArraysEqual( |
27 | + gatherNth(-1, 3), |
28 | + [3, 2, 1]); |
29 | + |
30 | + self.assertArraysEqual( |
31 | + gatherNth(1, 0), |
32 | + seq); |
33 | + |
34 | + self.assertArraysEqual( |
35 | + gatherNth(2, 0), |
36 | + [2, 4, 6, 8, 10]); |
37 | + |
38 | + self.assertArraysEqual( |
39 | + gatherNth(2, 1), |
40 | + [1, 3, 5, 7, 9]); |
41 | + |
42 | + self.assertArraysEqual( |
43 | + gatherNth(3, 3), |
44 | + [3, 6, 9]); |
45 | + |
46 | + self.assertArraysEqual( |
47 | + gatherNth(100, 0), |
48 | + []); |
49 | + |
50 | + self.assertArraysEqual( |
51 | + gatherNth(100, 1), |
52 | + [1]); |
53 | + }, |
54 | + |
55 | + |
56 | + /** |
57 | + * L{Methanal.Util.filter} gathers only those elements from a sequence for |
58 | + * which the predicate is true. A predicate of C{null} will filter values |
59 | + * whose truth value is false, e.g. C{0}, C{''} etc. A predicate of |
60 | + * C{undefined} will filter values that are C{null} or C{undefined}. |
61 | + * Otherwise the predicate must be a function. |
62 | + */ |
63 | + function test_filter(self) { |
64 | + var filter = Methanal.Util.filter; |
65 | + var seq = [0, undefined, 1, null, 2, '', 3]; |
66 | + |
67 | + function isOdd(x) { |
68 | + return x % 2 !== 0; |
69 | + }; |
70 | + |
71 | + self.assertArraysEqual( |
72 | + filter(null, seq), |
73 | + [1, 2, 3]); |
74 | + |
75 | + self.assertArraysEqual( |
76 | + filter(undefined, seq), |
77 | + [0, 1, 2, '', 3]); |
78 | + |
79 | + self.assertArraysEqual( |
80 | + filter(isOdd, filter(null, seq)), |
81 | + [1, 3]); |
82 | + |
83 | + self.assertThrows(Error, |
84 | + function () { filter('hello', seq); }); |
85 | }); |
86 | |
87 | |
88 | |
89 | === modified file 'methanal/js/Methanal/Tests/TestView.js' |
90 | --- methanal/js/Methanal/Tests/TestView.js 2010-03-09 08:57:31 +0000 |
91 | +++ methanal/js/Methanal/Tests/TestView.js 2010-07-08 18:27:43 +0000 |
92 | @@ -43,7 +43,7 @@ |
93 | * Create a C{Methanal.View.LiveForm}. |
94 | */ |
95 | function createForm(self, viewOnly) { |
96 | - var controlNames = {}; |
97 | + var controlNames = []; |
98 | form = Methanal.Tests.TestView.MockLiveForm(controlNames, viewOnly); |
99 | Methanal.Util.nodeInserted(form); |
100 | return form; |
101 | @@ -169,9 +169,9 @@ |
102 | function testControls(self, controls, testingFunc) { |
103 | var map = Methanal.Util.map; |
104 | |
105 | - var controlNames = {}; |
106 | + var controlNames = []; |
107 | map(function (control) { |
108 | - controlNames[control.name] = 1; |
109 | + controlNames.push(control.name); |
110 | }, controls); |
111 | |
112 | var form = Methanal.Tests.TestView.MockLiveForm(controlNames); |
113 | |
114 | === modified file 'methanal/js/Methanal/Util.js' |
115 | --- methanal/js/Methanal/Util.js 2010-03-16 11:55:09 +0000 |
116 | +++ methanal/js/Methanal/Util.js 2010-07-08 18:27:43 +0000 |
117 | @@ -365,6 +365,8 @@ |
118 | |
119 | /** |
120 | * Apply C{f} over each value in C{seq} and gather the results. |
121 | + * |
122 | + * @rtype: C{Array} |
123 | */ |
124 | Methanal.Util.map = function map(f, seq) { |
125 | if (typeof f !== 'function') { |
126 | @@ -380,6 +382,39 @@ |
127 | |
128 | |
129 | /** |
130 | + * Gather elements from C{seq} for which C{pred(seq[index])} is C{true}. |
131 | + * |
132 | + * There are two special cases for C{pred}: |
133 | + * |
134 | + * 1. C{null} means filter out things that are not a true value (like |
135 | + * filter(None, seq) in Python), e.g. C{null}, C{undefined}, C{''}, C{0}, |
136 | + * etc. |
137 | + * |
138 | + * 2. C{undefined} means filter out things that are C{null} or C{undefined}. |
139 | + * |
140 | + * @rtype: C{Array} |
141 | + */ |
142 | +Methanal.Util.filter = function filter(pred, seq) { |
143 | + if (pred === null) { |
144 | + pred = function (x) { return !!x; } |
145 | + } else if (pred === undefined) { |
146 | + pred = function (x) { return x !== null && x !== undefined; } |
147 | + } else if (typeof pred !== 'function') { |
148 | + throw new Error('"pred" must be a function, null or undefined'); |
149 | + } |
150 | + var results = []; |
151 | + for (var i = 0; i < seq.length; ++i) { |
152 | + var x = seq[i]; |
153 | + if (pred(x)) { |
154 | + results.push(x); |
155 | + } |
156 | + } |
157 | + return results; |
158 | +}; |
159 | + |
160 | + |
161 | + |
162 | +/** |
163 | * Find the quotient and remainder of two numbers. |
164 | */ |
165 | Methanal.Util.divmod = function divmod(x, y) { |
166 | @@ -441,6 +476,30 @@ |
167 | |
168 | |
169 | /** |
170 | + * Transform an array of strings into a mapping. |
171 | + * |
172 | + * This has the effect of creating a collection of unique C{String} elements, |
173 | + * like a set. For a more complete set implementation consider using |
174 | + * L{Methanal.Util.StringSet}. |
175 | + * |
176 | + * @type seq: C{Array} of C{String} |
177 | + * @param seq: Strings to initialise the set with. |
178 | + * |
179 | + * @rtype: C{object} mapping C{String} to C{Boolean} |
180 | + */ |
181 | +Methanal.Util.arrayToMapping = function arrayToMapping(seq) { |
182 | + var s = {}; |
183 | + if (seq) { |
184 | + for (var i = 0; i < seq.length; ++i) { |
185 | + s[seq[i]] = true; |
186 | + } |
187 | + } |
188 | + return s; |
189 | +}; |
190 | + |
191 | + |
192 | + |
193 | +/** |
194 | * An unordered collection of unique C{String} elements. |
195 | */ |
196 | Divmod.Class.subclass(Methanal.Util, 'StringSet').methods( |
197 | @@ -451,13 +510,7 @@ |
198 | * @param seq: Strings to initialise the set with |
199 | */ |
200 | function __init__(self, seq) { |
201 | - var s = {}; |
202 | - if (seq) { |
203 | - for (var i = 0; i < seq.length; ++i) { |
204 | - s[seq[i]] = true; |
205 | - } |
206 | - } |
207 | - self._set = s; |
208 | + self._set = Methanal.Util.arrayToMapping(seq); |
209 | }, |
210 | |
211 | |
212 | @@ -876,3 +929,43 @@ |
213 | return node; |
214 | }; |
215 | }; |
216 | + |
217 | + |
218 | + |
219 | +/** |
220 | + * Apply a function over every nth item in a sequence. |
221 | + * |
222 | + * Selection is made by evaluating C{an + b}, where C{n} is each index in |
223 | + * C{seq}. For example, every odd (first, third, fifth, etc.) item can be |
224 | + * selected with C{a=2} and C{b=1}, and every even (second, fourth, sixth, |
225 | + * etc.) item can be selected with C{a=2} and C{b=0}. |
226 | + * |
227 | + * This function is intended to match the CSS3 C{:nth-child()} pseudo-selector, |
228 | + * and as such treats the values C{a} and C{b} as 1-based values. It can be |
229 | + * less confusing to think about the indices as the first, second, etc. as |
230 | + * opposed to the element at index 0, 1, etc. |
231 | + * |
232 | + * @type seq: C{Array} |
233 | + * @param seq: Sequence from which to select elements from. |
234 | + * |
235 | + * @type a: C{Number} |
236 | + * @param a: Integer value, effectively used to divide C{seq} into groups of |
237 | + * C{a} elements. |
238 | + * |
239 | + * @type b: C{Number} |
240 | + * @param b: Integer value, effectived used to select the C{bth} value in each |
241 | + * group of C{a} elements. |
242 | + * |
243 | + * @type fn: C{function} taking 1 argument |
244 | + * @param fn: Function to apply to every nth item in C{seq}. |
245 | + * |
246 | + * @see: U{http://www.w3.org/TR/css3-selectors/#nth-child-pseudo} |
247 | + */ |
248 | +Methanal.Util.nthItem = function nthItem(seq, a, b, fn) { |
249 | + for (var n = 0; n <= seq.length; ++n) { |
250 | + var index = (a * n + b) - 1; |
251 | + if (index >= 0 && index < seq.length) { |
252 | + fn(seq[index]); |
253 | + } |
254 | + } |
255 | +}; |
256 | |
257 | === modified file 'methanal/js/Methanal/View.js' |
258 | --- methanal/js/Methanal/View.js 2010-07-06 14:14:44 +0000 |
259 | +++ methanal/js/Methanal/View.js 2010-07-08 18:27:43 +0000 |
260 | @@ -96,7 +96,9 @@ |
261 | * |
262 | * @type update: C{function} taking C{String}, C{Array} |
263 | * @ivar update: Called for each output control name with a sequence of values |
264 | - * from handler functions |
265 | + * from handler functions, which can optionally return a C{boolean} |
266 | + * indicating whether anything changed in an update, which is then returned |
267 | + * from L{changed}. |
268 | */ |
269 | Divmod.Class.subclass(Methanal.View, '_HandlerCache').methods( |
270 | function __init__(self, getData, update) { |
271 | @@ -117,6 +119,7 @@ |
272 | * relevant |
273 | */ |
274 | function _updateOutputs(self, outputs) { |
275 | + var updated = false; |
276 | for (var output in outputs) { |
277 | var results = []; |
278 | for (var handlerID in self._outputToHandlers[output]) { |
279 | @@ -124,9 +127,10 @@ |
280 | results.push(handler.value); |
281 | } |
282 | if (results.length > 0) { |
283 | - self.update(output, results); |
284 | + updated |= self.update(output, results); |
285 | } |
286 | } |
287 | + return updated; |
288 | }, |
289 | |
290 | |
291 | @@ -198,6 +202,9 @@ |
292 | * |
293 | * @type input: C{String} |
294 | * @param input: Input control name |
295 | + * |
296 | + * @rtype: C{boolean} |
297 | + * @return: Did any changes in the outputs occur during the updates? |
298 | */ |
299 | function changed(self, input) { |
300 | var handlers = self._inputToHandlers[input]; |
301 | @@ -210,7 +217,7 @@ |
302 | } |
303 | } |
304 | |
305 | - self._updateOutputs(outputs); |
306 | + return self._updateOutputs(outputs); |
307 | }); |
308 | |
309 | |
310 | @@ -251,6 +258,9 @@ |
311 | * @type fullyLoaded: C{boolean} |
312 | * @ivar fullyLoaded: Have all the form inputs reported that they're finished |
313 | * loading? |
314 | + * |
315 | + * @type enableControlStriping: C{boolean} |
316 | + * @ivar enableControlStriping: Perform visual striping of active form controls? |
317 | */ |
318 | Nevow.Athena.Widget.subclass(Methanal.View, 'FormBehaviour').methods( |
319 | /** |
320 | @@ -288,8 +298,10 @@ |
321 | var control = self.getControl(name); |
322 | result = Methanal.Util.reduce(_and, values, true); |
323 | self.freeze(); |
324 | + var changed = result !== control.active; |
325 | control.setActive(result); |
326 | self.thaw(); |
327 | + return changed; |
328 | }, |
329 | |
330 | |
331 | @@ -311,11 +323,11 @@ |
332 | |
333 | self._validatorCache = Methanal.View._HandlerCache( |
334 | getData, |
335 | - function (name, values) { self._validatorUpdate(name, values); }); |
336 | + function (name, values) { return self._validatorUpdate(name, values); }); |
337 | |
338 | self._depCache = Methanal.View._HandlerCache( |
339 | getData, |
340 | - function (name, values) { self._depUpdate(name, values); }); |
341 | + function (name, values) { return self._depUpdate(name, values); }); |
342 | |
343 | self.controlsLoaded = false; |
344 | self.fullyLoaded = false; |
345 | @@ -406,6 +418,37 @@ |
346 | |
347 | |
348 | /** |
349 | + * Perform control striping. |
350 | + * |
351 | + * If L{enableControlStriping} is C{false}, striping is not performed. |
352 | + */ |
353 | + function _stripeControls(self) { |
354 | + if (self.enableControlStriping) { |
355 | + self.stripeControls(); |
356 | + } |
357 | + }, |
358 | + |
359 | + |
360 | + /** |
361 | + * Perform control striping. |
362 | + */ |
363 | + function stripeControls(self) { |
364 | + var activeControlNames = Methanal.Util.filter(function (name) { |
365 | + var control = self.getControl(name); |
366 | + Methanal.Util.removeElementClass( |
367 | + control.widgetParent.node, 'methanal-control-even'); |
368 | + return control.active; |
369 | + }, self._controlNamesOrdered); |
370 | + |
371 | + Methanal.Util.nthItem(activeControlNames, 2, 0, |
372 | + function (controlName) { |
373 | + var node = self.getControl(controlName).widgetParent.node; |
374 | + Methanal.Util.addElementClass(node, 'methanal-control-even'); |
375 | + }); |
376 | + }, |
377 | + |
378 | + |
379 | + /** |
380 | * Perform final form initialisation tasks. |
381 | * |
382 | * Once all controls in L{controlNames} have reported in and L{setActions} |
383 | @@ -423,6 +466,7 @@ |
384 | Methanal.Util.addElementClass(node, 'dependancy-parent'); |
385 | } |
386 | self.refresh(); |
387 | + self._stripeControls(); |
388 | }, |
389 | |
390 | |
391 | @@ -503,8 +547,11 @@ |
392 | * An event that is called when a form input's value is changed. |
393 | */ |
394 | function valueChanged(self, control) { |
395 | - self._depCache.changed(control.name); |
396 | + var depsChanged = self._depCache.changed(control.name); |
397 | self.validate(control); |
398 | + if (depsChanged) { |
399 | + self._stripeControls(); |
400 | + } |
401 | }, |
402 | |
403 | |
404 | @@ -748,9 +795,14 @@ |
405 | */ |
406 | Methanal.View.FormBehaviour.subclass(Methanal.View, 'LiveForm').methods( |
407 | function __init__(self, node, viewOnly, controlNames) { |
408 | + self.enableControlStriping = true; |
409 | Methanal.View.LiveForm.upcall(self, '__init__', node); |
410 | self.viewOnly = viewOnly; |
411 | - self.controlNames = controlNames; |
412 | + if (!(controlNames instanceof Array)) { |
413 | + throw new Error('"controlNames" must be an Array of control names'); |
414 | + } |
415 | + self.controlNames = Methanal.Util.arrayToMapping(controlNames); |
416 | + self._controlNamesOrdered = controlNames; |
417 | self.formInit(); |
418 | }, |
419 | |
420 | @@ -1126,6 +1178,10 @@ |
421 | self.name = name; |
422 | self.setValid(); |
423 | self.controlNames = controlNames; |
424 | + self._controlNamesOrdered = []; |
425 | + for (var key in self.controlNames) { |
426 | + self._controlNamesOrdered.push(key); |
427 | + } |
428 | }, |
429 | |
430 | |
431 | |
432 | === modified file 'methanal/static/styles/methanal.css' |
433 | --- methanal/static/styles/methanal.css 2010-07-06 14:14:44 +0000 |
434 | +++ methanal/static/styles/methanal.css 2010-07-08 18:27:43 +0000 |
435 | @@ -15,8 +15,12 @@ |
436 | float: left; |
437 | } |
438 | |
439 | -.methanal-control-odd { |
440 | - background-color: #ddd; |
441 | +.methanal-form-group .methanal-control-even { |
442 | + background-color: #d3d3ea; |
443 | +} |
444 | + |
445 | +.methanal-control-even { |
446 | + background-color: #f0f0fc; |
447 | } |
448 | |
449 | .methanal-control-error { |
450 | |
451 | === modified file 'methanal/view.py' |
452 | --- methanal/view.py 2010-03-11 08:43:14 +0000 |
453 | +++ methanal/view.py 2010-07-08 18:27:43 +0000 |
454 | @@ -172,8 +172,8 @@ |
455 | |
456 | |
457 | def getInitialArguments(self): |
458 | - keys = (c.name for c in self.getAllControls()) |
459 | - return [dict.fromkeys(keys, 1)] |
460 | + keys = [c.name for c in self.getAllControls()] |
461 | + return [keys] |
462 | |
463 | |
464 | def getAllControls(self): |
Sorry to be a pain, but after actually investigating restriping on dep changes it turns out that it's not slow on IE and wasn't very hard to implement.