]> err.no Git - scalable-opengroupware.org/blob - SOGo/UI/Common/UIxTabView.m
completed UIxCalWeekview and some refactoring
[scalable-opengroupware.org] / SOGo / UI / Common / UIxTabView.m
1 /*
2   Copyright (C) 2000-2004 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
6   OGo is free software; you can redistribute it and/or modify it under
7   the terms of the GNU Lesser General Public License as published by the
8   Free Software Foundation; either version 2, or (at your option) any
9   later version.
10
11   OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12   WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14   License for more details.
15
16   You should have received a copy of the GNU Lesser General Public
17   License along with OGo; see the file COPYING.  If not, write to the
18   Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
19   02111-1307, USA.
20 */
21 // $Id$
22
23 #include "UIxTabView.h"
24 #include "common.h"
25 #import <NGObjWeb/NGObjWeb.h>
26 #import <NGExtensions/NGExtensions.h>
27 #import <EOControl/EOControl.h>
28 #include <WEExtensions/WEClientCapabilities.h>
29
30 #if DEBUG
31 // #  define DEBUG_TAKEVALUES 1
32 #  define DEBUG_JS 1
33 #endif
34
35 /* context keys */
36 NSString *UIxTabView_HEAD      = @"UIxTabView_head";
37 NSString *UIxTabView_BODY      = @"UIxTabView_body";
38 NSString *UIxTabView_KEYS      = @"UIxTabView_keys";
39 NSString *UIxTabView_SCRIPT    = @"UIxTabView_script";
40 NSString *UIxTabView_ACTIVEKEY = @"UIxTabView_activekey";
41 NSString *UIxTabView_COLLECT   = @"~tv~";
42
43 @implementation UIxTabView
44
45 static NSNumber *YesNumber;
46
47 + (void)initialize {
48   if (YesNumber == nil)
49     YesNumber = [[NSNumber numberWithBool:YES] retain];
50 }
51
52 + (int)version {
53   return [super version] + 0;
54 }
55
56 - (id)initWithName:(NSString *)_name
57   associations:(NSDictionary *)_config
58   template:(WOElement *)_subs
59 {
60   if ((self = [super initWithName:_name associations:_config template:_subs])) {
61     self->selection          = WOExtGetProperty(_config, @"selection");
62     
63     self->headerStyle        = WOExtGetProperty(_config, @"headerStyle");
64     self->bodyStyle          = WOExtGetProperty(_config, @"bodyStyle");
65     self->tabStyle           = WOExtGetProperty(_config, @"tabStyle");
66     self->selectedTabStyle   = WOExtGetProperty(_config, @"selectedTabStyle");
67
68     self->bgColor            = WOExtGetProperty(_config, @"bgColor");
69     self->nonSelectedBgColor = WOExtGetProperty(_config, @"nonSelectedBgColor");
70     self->leftCornerIcon     = WOExtGetProperty(_config, @"leftCornerIcon");
71     self->rightCornerIcon    = WOExtGetProperty(_config, @"rightCornerIcon");
72
73     self->tabIcon            = WOExtGetProperty(_config, @"tabIcon");
74     self->leftTabIcon        = WOExtGetProperty(_config, @"leftTabIcon");
75     self->selectedTabIcon    = WOExtGetProperty(_config, @"selectedTabIcon");
76
77     self->asBackground       = WOExtGetProperty(_config, @"asBackground");
78     self->width              = WOExtGetProperty(_config, @"width");
79     self->height             = WOExtGetProperty(_config, @"height");
80     self->activeBgColor      = WOExtGetProperty(_config, @"activeBgColor");
81     self->inactiveBgColor    = WOExtGetProperty(_config, @"inactiveBgColor");
82
83     self->fontColor          = WOExtGetProperty(_config, @"fontColor");
84     self->fontSize           = WOExtGetProperty(_config, @"fontSize");
85     self->fontFace           = WOExtGetProperty(_config, @"fontFace");
86
87     self->template = RETAIN(_subs);
88   }
89   return self;
90 }
91
92 - (void)dealloc {
93   [self->selection release];
94
95   [self->headerStyle release];
96   [self->bodyStyle release];
97   [self->tabStyle release];
98   [self->selectedTabStyle release];
99
100   RELEASE(self->bgColor);
101   RELEASE(self->nonSelectedBgColor);
102   RELEASE(self->leftCornerIcon);
103   RELEASE(self->rightCornerIcon);
104
105   RELEASE(self->leftTabIcon);
106   RELEASE(self->selectedTabIcon);
107   RELEASE(self->tabIcon);
108
109   RELEASE(self->width);
110   RELEASE(self->height);
111
112   RELEASE(self->activeBgColor);
113   RELEASE(self->inactiveBgColor);
114
115   RELEASE(self->fontColor);
116   RELEASE(self->fontSize);
117   RELEASE(self->fontFace);
118   
119   RELEASE(self->template);
120   [super dealloc];
121 }
122
123 /* nesting */
124
125 - (id)saveNestedStateInContext:(WOContext *)_ctx {
126   return nil;
127 }
128 - (void)restoreNestedState:(id)_state inContext:(WOContext *)_ctx {
129   if (_state == nil) return;
130 }
131
132 - (NSArray *)collectKeysInContext:(WOContext *)_ctx {
133   /* collect mode, collects all keys */
134   [_ctx setObject:UIxTabView_COLLECT forKey:UIxTabView_HEAD];
135   
136   [self->template appendToResponse:nil inContext:_ctx];
137   
138   [_ctx removeObjectForKey:UIxTabView_HEAD];
139   return [_ctx objectForKey:UIxTabView_KEYS];
140 }
141
142 /* responder */
143
144 - (void)takeValuesFromRequest:(WORequest *)_req inContext:(WOContext *)_ctx {
145   id       nestedState;
146   NSString *activeTabKey;
147   
148   activeTabKey = [self->selection stringValueInComponent:[_ctx component]];
149   NSLog(@"%s activeTabKey:%@", __PRETTY_FUNCTION__, activeTabKey);
150   nestedState = [self saveNestedStateInContext:_ctx];
151   [_ctx appendElementIDComponent:@"b"];
152   [_ctx appendElementIDComponent:activeTabKey];
153   
154   [_ctx setObject:activeTabKey forKey:UIxTabView_BODY];
155   
156 #if DEBUG_TAKEVALUES
157   [[_ctx component] debugWithFormat:@"UIxTabView: body takes values, eid='%@'",
158                     [_ctx elementID]];
159 #endif
160   
161   [self->template takeValuesFromRequest:_req inContext:_ctx];
162   
163   [_ctx removeObjectForKey:UIxTabView_BODY];
164   [_ctx deleteLastElementIDComponent]; // activeKey
165   [_ctx deleteLastElementIDComponent]; /* 'b' */
166   [self restoreNestedState:nestedState inContext:_ctx];
167 }
168
169 - (id)invokeActionForRequest:(WORequest *)_req inContext:(WOContext *)_ctx {
170   NSString *key;
171   id       result;
172   id       nestedState;
173   
174   if ((key = [_ctx currentElementID]) == nil)
175     return nil;
176   
177   result      = nil;
178   nestedState = [self saveNestedStateInContext:_ctx];
179     
180   if ([key isEqualToString:@"h"]) {
181     /* header action */
182     //NSString *urlKey;
183     
184     [_ctx consumeElementID];
185     [_ctx appendElementIDComponent:@"h"];
186 #if 0
187     if ((urlKey = [_ctx currentElementID]) == nil) {
188       [[_ctx application]
189              debugWithFormat:@"missing active head tab key !"];
190     }
191     else {
192       //NSLog(@"clicked: %@", urlKey);
193       [_ctx consumeElementID];
194       [_ctx appendElementIDComponent:urlKey];
195     }
196 #endif
197     
198     [_ctx setObject:self->selection forKey:UIxTabView_HEAD];
199     result = [self->template invokeActionForRequest:_req inContext:_ctx];
200     [_ctx removeObjectForKey:UIxTabView_HEAD];
201
202 #if 0
203     if (urlKey)
204       [_ctx deleteLastElementIDComponent]; // active key
205 #endif
206     [_ctx deleteLastElementIDComponent]; // 'h'
207   }
208   else if ([key isEqualToString:@"b"]) {
209     /* body action */
210     NSString *activeTabKey, *urlKey;
211     
212     [_ctx consumeElementID];
213     [_ctx appendElementIDComponent:@"b"];
214       
215     if ((urlKey = [_ctx currentElementID]) == nil) {
216       [[_ctx application]
217              debugWithFormat:@"missing active body tab key !"];
218     }
219     else {
220       //NSLog(@"clicked: %@", urlKey);
221       [_ctx consumeElementID];
222       [_ctx appendElementIDComponent:urlKey];
223     }
224     
225     activeTabKey = [self->selection stringValueInComponent:[_ctx component]];
226     [_ctx setObject:activeTabKey forKey:UIxTabView_BODY];
227     
228     result = [self->template invokeActionForRequest:_req inContext:_ctx];
229       
230     [_ctx removeObjectForKey:UIxTabView_BODY];
231
232     if (urlKey)
233       [_ctx deleteLastElementIDComponent]; // active key
234     [_ctx deleteLastElementIDComponent]; // 'b'
235   }
236   else {
237     [[_ctx application]
238            debugWithFormat:@"unknown tab container key '%@'", key];
239   }
240     
241   [self restoreNestedState:nestedState inContext:_ctx];
242   return result;
243 }
244
245 - (NSString *)_tabViewCountInContext:(WOContext *)_ctx {
246   int count;
247   count = [[_ctx valueForKey:@"UIxTabViewScriptDone"] intValue];
248   return [NSString stringWithFormat:@"%d",count];
249 }
250
251 - (NSString *)scriptHref:(UIxTabItemInfo *)_info
252   inContext:(WOContext *)_ctx
253   isLeft:(BOOL)_isLeft
254   keys:(NSArray *)_keys
255 {
256   NSMutableString *result = [NSMutableString string];
257   UIxTabItemInfo *tmp;
258   NSString       *activeKey;
259   int            i, cnt;
260   NSString       *elID;
261   NSString       *tstring;
262   
263   activeKey = [self->selection stringValueInComponent:[_ctx component]];
264   [result appendString:@"JavaScript:showTab("];
265   [result appendString:_info->key];
266   [result appendString:@"Tab);"];
267   
268   [result appendString:@"swapCorners("];
269   tstring = (!_isLeft)
270     ? @"tabCorner%@,tabCornerLeft%@);"
271     : @"tabCornerLeft%@,tabCorner%@);";
272   elID = [self _tabViewCountInContext:_ctx];
273   [result appendString:[NSString stringWithFormat:tstring,elID,elID]];
274   
275   for (i=0, cnt = [_keys count]; i < cnt; i++) {
276     tmp = [_keys objectAtIndex:i];
277
278     if ((tmp->isScript || [tmp->key isEqualToString:activeKey])
279         && ![tmp->key isEqualToString:_info->key]) {
280       [result appendString:@"hideTab("];
281       [result appendString:tmp->key];
282       [result appendString:@"Tab);"];
283     }
284   }
285   return result;
286 }
287
288 - (void)appendLink:(UIxTabItemInfo *)_info
289   toResponse:(WOResponse *)_response
290   inContext:(WOContext *)_ctx
291   isActive:(BOOL)_isActive isLeft:(BOOL)_isLeft
292   doScript:(BOOL)_doScript keys:(NSArray *)_keys
293 {
294   NSString *headUri    = nil;
295   NSString *label      = nil;
296   NSString *styleName  = nil;
297   WEClientCapabilities *ccaps;
298   WOComponent *comp;
299
300   ccaps = [[_ctx request] clientCapabilities];
301
302   comp = [_ctx component];
303   headUri = _info->uri;
304
305   if ((label = _info->label) == nil)
306     label = _info->key;
307   
308   if (_isActive) {
309     styleName = (_info->selectedTabStyle)
310       ? _info->selectedTabStyle
311       : [self->selectedTabStyle stringValueInComponent:comp];
312   }
313   else {
314     styleName = (_info->tabStyle)
315       ? _info->tabStyle
316       : [self->tabStyle stringValueInComponent:comp];
317   }
318   
319   [_response appendContentString:@"<td align='center' valign='middle'"];
320   
321   if (styleName) {
322       [_response appendContentString:@" class='"];
323       [_response appendContentHTMLAttributeValue:styleName];
324       [_response appendContentCharacter:'\''];
325   }
326
327   // click on td background
328   if ([ccaps isInternetExplorer] && [ccaps isJavaScriptBrowser]) {
329       [_response appendContentString:@" onclick=\"window.location.href='"];
330       [_response appendContentHTMLAttributeValue:headUri];
331       [_response appendContentString:@"'\""];
332   }
333   
334   [_response appendContentCharacter:'>'];
335
336   [_response appendContentString:@"<a href=\""];
337   
338   [_response appendContentHTMLAttributeValue:headUri];
339   
340   [_response appendContentString:@"\" "];
341   [_response appendContentString:
342                [NSString stringWithFormat:@"name='%@TabLink'", _info->key]];
343   [_response appendContentString:@">"];
344   
345   if ([label length] < 1)
346       label = _info->key;
347   [_response appendContentString:@"<nobr>"];
348   [_response appendContentHTMLString:label];
349   [_response appendContentString:@"</nobr>"];
350   
351   [_response appendContentString:@"</a>"];
352
353   [_response appendContentString:@"</td>"];
354 }
355
356 - (void)appendSubmitButton:(UIxTabItemInfo *)_info
357   toResponse:(WOResponse *)_response
358   inContext:(WOContext *)_ctx
359   isActive:(BOOL)_isActive isLeft:(BOOL)_left
360   doScript:(BOOL)_doScript   keys:(NSArray *)_keys
361 {
362   [self appendLink:_info
363         toResponse:_response
364         inContext:_ctx
365         isActive:_isActive isLeft:_left
366         doScript:NO keys:_keys];
367 }
368
369 - (void)_appendTabViewJSScriptToResponse:(WOResponse *)_response
370   inContext:(WOContext *)_ctx
371 {
372   [_response appendContentString:
373                @"<script language=\"JavaScript\">\n<!--\n\n"
374                @"function showTab(obj) {\n"
375 #if DEBUG_JS
376                @"  if (obj==null) { alert('missing tab obj ..'); return; }\n"
377                @"  if (obj['Div']==null) {"
378                @"    alert('missing div key in ' + obj); return; }\n"
379                @"  if (obj['Div'].style==null) {"
380                @"    alert('missing style key in div ' + obj['Div']);return; }\n"
381 #endif
382                @"  obj['Div'].style.display = \"\";\n"
383                @"  obj['Img'].src = obj[\"Ar\"][1].src;\n"
384                @"  obj['link'].href = obj[\"href2\"];\n"
385                @"}\n"
386                @"function hideTab(obj) {\n"
387 #if DEBUG_JS
388                @"  if (obj==null) { alert('missing tab obj ..'); return; }\n"
389                @"  if (obj['Div']==null) {"
390                @"    alert('missing div key in ' + obj); return; }\n"
391                @"  if (obj['Div'].style==null) {"
392                @"    alert('missing style key in div ' + obj['Div']);return; }\n"
393 #endif
394                @" obj['Div'].style.display = \"none\";\n"
395                @" obj['Img'].src = obj[\"Ar\"][0].src;\n"
396                @" obj['link'].href = obj[\"href1\"];\n"
397                @"}\n"
398                @"function swapCorners(obj1,obj2) {\n"
399                @"   if (obj1==null) { alert('missing corner 1'); return; }\n"
400                @"   if (obj2==null) { alert('missing corner 2'); return; }\n"
401                @"   obj1.style.display = \"none\";\n"
402                @"   obj2.style.display = \"\";\n"
403                @"}\n"
404                @"//-->\n</script>"];
405 }
406
407 - (void)_appendHeaderRowToResponse:(WOResponse *)_response
408   inContext:(WOContext *)_ctx
409   keys:(NSArray *)keys activeKey:(NSString *)activeKey
410   doScript:(BOOL)doScript
411 {
412   unsigned  i, count;
413   BOOL      doForm;
414   NSString  *styleName;
415   
416   doForm = NO;  /* generate form controls ? */
417   
418   [_response appendContentString:@"<tr><td colspan='2'>"];
419   
420   styleName = [self->headerStyle stringValueInComponent:[_ctx component]];
421   if(styleName) {
422       [_response appendContentString:
423           @"<table border='0' cellpadding='0' cellspacing='0' class='"];
424       [_response appendContentHTMLAttributeValue:styleName];
425       [_response appendContentString:@"'><tr>"];
426   }
427   else {
428       [_response appendContentString:
429           @"<table border='0' cellpadding='0' cellspacing='0'><tr>"];
430   }
431
432   for (i = 0, count = [keys count]; i < count; i++) {
433     UIxTabItemInfo *info;
434     NSString       *key;
435     BOOL           isActive;
436     
437     info     = [keys objectAtIndex:i];
438     key      = info->key;
439     isActive = [key isEqualToString:activeKey];
440     
441     [_ctx appendElementIDComponent:key];
442     
443     if (doForm) {
444       /* tab is inside of a FORM, so produce submit buttons */
445       [self appendSubmitButton:info
446             toResponse:_response
447             inContext:_ctx
448             isActive:isActive
449             isLeft:(i == 0) ? YES : NO
450             doScript:NO
451             keys:keys];
452     }
453     else {
454       /* tab is not in a FORM, generate hyperlinks for tab */
455       [self appendLink:info
456             toResponse:_response
457             inContext:_ctx
458             isActive:isActive
459             isLeft:(i == 0) ? YES : NO
460             doScript:NO
461             keys:keys];
462     }
463     
464     [_ctx deleteLastElementIDComponent];
465   }
466   //  [_response appendContentString:@"<td></td>"];
467   [_response appendContentString:@"</tr></table>"];
468   [_response appendContentString:@"</td></tr>"];
469 }
470
471 - (void)_appendHeaderFootRowToResponse:(WOResponse *)_response
472   inContext:(WOContext *)_ctx
473   bgcolor:(NSString *)bgcolor
474   doScript:(BOOL)doScript
475   isLeftActive:(BOOL)isLeftActive
476 {
477   NSString *styleName;
478   [_response appendContentString:@"  <tr"];
479     
480   styleName = [self->bodyStyle stringValueInComponent:[_ctx component]];
481   if(styleName) {
482     [_response appendContentString:@" class='"];
483     [_response appendContentHTMLAttributeValue:styleName];
484     [_response appendContentCharacter:'\''];
485   }
486   if (bgcolor) {
487     [_response appendContentString:@" bgcolor=\""];
488     [_response appendContentHTMLAttributeValue:bgcolor];
489     [_response appendContentString:@"\""];
490   }
491   [_response appendContentString:@">\n"];
492     
493   /* left corner */
494   [_response appendContentString:@"    <td align=\"left\" width=\"10\">"];
495   
496   if (isLeftActive)
497     [_response appendContentString:@"&nbsp;"];
498   
499   if (!isLeftActive) {
500     NSString *uri;
501     
502     uri = [self->leftCornerIcon stringValueInComponent:[_ctx component]];
503     if ((uri = WEUriOfResource(uri, _ctx))) {
504       [_response appendContentString:@"<img border=\"0\" alt=\"\" src=\""];
505       [_response appendContentString:uri];
506       [_response appendContentString:@"\" />"];
507     }
508     else
509       [_response appendContentString:@"&nbsp;"];
510   }
511   
512   [_response appendContentString:@"</td>"];
513
514   /* right corner */
515   [_response appendContentString:@"    <td align=\"right\">"];
516   {
517     NSString *uri;
518       
519     uri = [self->rightCornerIcon stringValueInComponent:[_ctx component]];
520     if ((uri = WEUriOfResource(uri, _ctx))) {
521       [_response appendContentString:@"<img border=\"0\" alt=\"\" src=\""];
522       [_response appendContentString:uri];
523       [_response appendContentString:@"\" />"];
524     }
525     else
526       [_response appendContentString:@"&nbsp;"];
527   }
528   [_response appendContentString:@"</td>\n"];
529     
530   [_response appendContentString:@"  </tr>\n"];
531 }
532
533 - (void)_appendBodyRowToResponse:(WOResponse *)_response
534   inContext:(WOContext *)_ctx
535   bgcolor:(NSString *)bgcolor
536   activeKey:(NSString *)activeKey
537 {
538   WEClientCapabilities *ccaps;
539   BOOL indentContent;
540   NSString *styleName;
541
542   styleName = [self->bodyStyle stringValueInComponent:[_ctx component]];
543   ccaps = [[_ctx request] clientCapabilities];
544
545   /* put additional padding table into content ??? */
546   indentContent = [ccaps isFastTableBrowser] && ![ccaps isTextModeBrowser];
547   
548   [_response appendContentString:@"<tr"];
549   if(styleName) {
550     [_response appendContentString:@" class='"];
551     [_response appendContentHTMLAttributeValue:styleName];
552     [_response appendContentCharacter:'\''];
553   }
554   [_response appendContentString:@"><td colspan='2'"];
555   if (bgcolor) {
556     [_response appendContentString:@" bgcolor=\""];
557     [_response appendContentHTMLAttributeValue:bgcolor];
558     [_response appendContentCharacter:'\"'];
559   }
560   [_response appendContentCharacter:'>'];
561     
562   if (indentContent) {
563     /* start padding table */
564     [_response appendContentString:
565                @"<table border='0' width='100%'"
566                @" cellpadding='10' cellspacing='0'>"];
567     [_response appendContentString:@"<tr><td>"];
568   }
569     
570   [_ctx appendElementIDComponent:@"b"];
571   [_ctx appendElementIDComponent:activeKey];
572   
573   /* generate currently active body */
574   {
575     [_ctx setObject:activeKey forKey:UIxTabView_BODY];
576     [self->template appendToResponse:_response inContext:_ctx];
577     [_ctx removeObjectForKey:UIxTabView_BODY];
578   }
579   
580   [_ctx deleteLastElementIDComponent]; // activeKey
581   [_ctx deleteLastElementIDComponent]; // 'b'
582     
583   if (indentContent)
584     /* close padding table */
585     [_response appendContentString:@"</td></tr></table>"];
586     
587   [_response appendContentString:@"</td></tr>"];
588 }
589
590 - (BOOL)isLeftActiveInKeys:(NSArray *)keys activeKey:(NSString *)activeKey{
591   unsigned i, count;
592   BOOL isLeftActive;
593   
594   isLeftActive = NO;
595   
596   for (i = 0, count = [keys count]; i < count; i++) {
597     UIxTabItemInfo *info;
598     
599     info = [keys objectAtIndex:i];
600     
601     if ((i == 0) && [info->key isEqualToString:activeKey])
602       isLeftActive = YES;
603   }
604   
605   return isLeftActive;
606 }
607
608 - (void)appendToResponse:(WOResponse *)_response inContext:(WOContext *)_ctx {
609   WOComponent  *cmp;
610   NSString     *bgcolor;
611   BOOL         isLeftActive;
612   id           nestedState;
613   NSString     *activeKey;
614   NSArray      *keys;
615   int          tabViewCount; /* used for image id's and writing script once */
616   
617   tabViewCount  = [[_ctx valueForKey:@"UIxTabViewScriptDone"] intValue];
618   cmp           = [_ctx component];
619   
620   /* save state */
621   
622   nestedState = [self saveNestedStateInContext:_ctx];
623   
624   /* configure */
625   
626   activeKey = [self->selection stringValueInComponent:cmp];
627   
628   bgcolor = [self->bgColor stringValueInComponent:cmp];
629   bgcolor = [bgcolor stringValue];
630   
631   [_ctx appendElementIDComponent:@"h"];
632   
633   /* collect & process keys (= available tabs) */
634   
635   keys = [self collectKeysInContext:_ctx];
636   
637   if (![[keys valueForKey:@"key"] containsObject:activeKey])
638     /* selection is not available in keys */
639     activeKey = nil;
640   
641   if ((activeKey == nil) && ([keys count] > 0)) {
642     /* no or invalid selection, use first key */
643     activeKey = [[keys objectAtIndex:0] key];
644     if ([self->selection isValueSettable])
645       [self->selection setValue:activeKey inComponent:[_ctx component]];
646   }
647   
648   /* start appending */
649   
650   /* count up for unique tabCorner/tabCornerLeft images */
651   [_ctx takeValue:[NSNumber numberWithInt:(tabViewCount + 1)]
652         forKey:@"UIxTabViewScriptDone"];
653   
654   [_response appendContentString:
655                @"<table border='0' width='100%'"
656                @" cellpadding='0' cellspacing='0'>"];
657   
658   /* find out whether left is active */
659   
660   isLeftActive = [self isLeftActiveInKeys:keys activeKey:activeKey];
661   
662   /* generate header row */
663   
664   [self _appendHeaderRowToResponse:_response inContext:_ctx
665         keys:keys activeKey:activeKey
666         doScript:NO];
667   
668   [_ctx deleteLastElementIDComponent]; // 'h' for head
669   [_ctx removeObjectForKey:UIxTabView_HEAD];
670
671   /* body row */
672   
673   [self _appendBodyRowToResponse:_response inContext:_ctx
674         bgcolor:bgcolor
675         activeKey:activeKey];
676   
677   /* close table */
678   
679   [_response appendContentString:@"</table>"];
680   [_ctx removeObjectForKey:UIxTabView_ACTIVEKEY];
681   [_ctx removeObjectForKey:UIxTabView_KEYS];
682   [self restoreNestedState:nestedState inContext:_ctx];
683 }
684
685 @end /* UIxTabView */