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