Merge lp:~methanal-developers/methanal/form-row-striping into lp:methanal

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
Reviewer Review Type Date Requested Status
Tristan Seligmann Needs Fixing
Review via email: mp+29297@code.launchpad.net
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 :

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.

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):

Subscribers

People subscribed via source and target branches