|
1 # copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
3 # |
|
4 # This file is part of CubicWeb. |
|
5 # |
|
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
|
7 # terms of the GNU Lesser General Public License as published by the Free |
|
8 # Software Foundation, either version 2.1 of the License, or (at your option) |
|
9 # any later version. |
|
10 # |
|
11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT |
|
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
14 # details. |
|
15 # |
|
16 # You should have received a copy of the GNU Lesser General Public License along |
|
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
|
18 """html widgets |
|
19 |
|
20 those are in cubicweb since we need to know available widgets at schema |
|
21 serialization time |
|
22 """ |
|
23 |
|
24 import random |
|
25 from math import floor |
|
26 |
|
27 from six import add_metaclass |
|
28 from six.moves import range |
|
29 |
|
30 from logilab.mtconverter import xml_escape |
|
31 from logilab.common.deprecation import class_deprecated |
|
32 |
|
33 from cubicweb.utils import UStringIO |
|
34 from cubicweb.uilib import toggle_action, htmlescape |
|
35 from cubicweb.web import jsonize |
|
36 from cubicweb.web.component import _bwcompatible_render_item |
|
37 |
|
38 # XXX HTMLWidgets should have access to req (for datadir / static urls, |
|
39 # i18n strings, etc.) |
|
40 class HTMLWidget(object): |
|
41 |
|
42 def _initialize_stream(self, w=None): |
|
43 if w: |
|
44 self.w = w |
|
45 else: |
|
46 self._stream = UStringIO() |
|
47 self.w = self._stream.write |
|
48 |
|
49 def _render(self): |
|
50 raise NotImplementedError |
|
51 |
|
52 def render(self, w=None): |
|
53 self._initialize_stream(w) |
|
54 self._render() |
|
55 if w is None: |
|
56 return self._stream.getvalue() |
|
57 |
|
58 def is_empty(self): |
|
59 return False |
|
60 |
|
61 |
|
62 class BoxWidget(HTMLWidget): # XXX Deprecated |
|
63 |
|
64 def __init__(self, title, id, items=None, _class="boxFrame", |
|
65 islist=True, shadow=True, escape=True): |
|
66 self.title = title |
|
67 self.id = id |
|
68 self.items = items or [] |
|
69 self._class = _class |
|
70 self.islist = islist |
|
71 self.shadow = shadow |
|
72 self.escape = escape |
|
73 |
|
74 def __len__(self): |
|
75 return len(self.items) |
|
76 |
|
77 def is_empty(self): |
|
78 return len(self) == 0 |
|
79 |
|
80 def append(self, item): |
|
81 self.items.append(item) |
|
82 |
|
83 def extend(self, items): |
|
84 self.items.extend(items) |
|
85 |
|
86 title_class = 'boxTitle' |
|
87 main_div_class = 'boxContent' |
|
88 listing_class = 'boxListing' |
|
89 |
|
90 def box_begin_content(self): |
|
91 self.w(u'<div class="%s">\n' % self.main_div_class) |
|
92 if self.islist: |
|
93 self.w(u'<ul class="%s">' % self.listing_class) |
|
94 |
|
95 def box_end_content(self): |
|
96 if self.islist: |
|
97 self.w(u'</ul>\n') |
|
98 self.w(u'</div>\n') |
|
99 if self.shadow: |
|
100 self.w(u'<div class="shadow"> </div>') |
|
101 |
|
102 def _render(self): |
|
103 if self.id: |
|
104 self.w(u'<div class="%s" id="%s">' % (self._class, self.id)) |
|
105 else: |
|
106 self.w(u'<div class="%s">' % self._class) |
|
107 if self.title: |
|
108 if self.escape: |
|
109 title = '<span>%s</span>' % xml_escape(self.title) |
|
110 else: |
|
111 title = '<span>%s</span>' % self.title |
|
112 self.w(u'<div class="%s">%s</div>' % (self.title_class, title)) |
|
113 if self.items: |
|
114 self.box_begin_content() |
|
115 for item in self.items: |
|
116 _bwcompatible_render_item(self.w, item) |
|
117 self.box_end_content() |
|
118 self.w(u'</div>') |
|
119 |
|
120 |
|
121 @add_metaclass(class_deprecated) |
|
122 class SideBoxWidget(BoxWidget): |
|
123 """default CubicWeb's sidebox widget""" |
|
124 __deprecation_warning__ = '[3.10] class %(cls)s is deprecated' |
|
125 |
|
126 title_class = u'sideBoxTitle' |
|
127 main_div_class = u'sideBoxBody' |
|
128 listing_class = '' |
|
129 |
|
130 def __init__(self, title, id=None): |
|
131 super(SideBoxWidget, self).__init__(title, id=id, _class='sideBox', |
|
132 shadow=False) |
|
133 |
|
134 |
|
135 class MenuWidget(BoxWidget): |
|
136 |
|
137 main_div_class = 'menuContent' |
|
138 listing_class = 'menuListing' |
|
139 |
|
140 def box_end_content(self): |
|
141 if self.islist: |
|
142 self.w(u'</ul>\n') |
|
143 self.w(u'</div>\n') |
|
144 |
|
145 |
|
146 class RawBoxItem(HTMLWidget): # XXX deprecated |
|
147 """a simple box item displaying raw data""" |
|
148 |
|
149 def __init__(self, label, liclass=None): |
|
150 self.label = label |
|
151 self.liclass = liclass |
|
152 |
|
153 def _start_li(self): |
|
154 if self.liclass is None: |
|
155 return u'<li>' |
|
156 else: |
|
157 return u'<li class="%s">' % self.liclass |
|
158 |
|
159 def _render(self): |
|
160 self.w(u'%s%s</li>' % (self._start_li(), self.label)) |
|
161 |
|
162 |
|
163 class BoxMenu(RawBoxItem): |
|
164 """a menu in a box""" |
|
165 |
|
166 link_class = 'boxMenu' |
|
167 |
|
168 def __init__(self, label, items=None, isitem=True, liclass=None, ident=None, |
|
169 link_class=None): |
|
170 super(BoxMenu, self).__init__(label, liclass) |
|
171 self.items = items or [] |
|
172 self.isitem = isitem |
|
173 self.ident = ident or u'boxmenu_%s' % label.replace(' ', '_').replace("'", '') |
|
174 if link_class: |
|
175 self.link_class = link_class |
|
176 |
|
177 def append(self, item): |
|
178 self.items.append(item) |
|
179 |
|
180 def _begin_menu(self, ident): |
|
181 self.w(u'<ul id="%s" class="hidden">' % ident) |
|
182 |
|
183 def _end_menu(self): |
|
184 self.w(u'</ul>') |
|
185 |
|
186 def _render(self): |
|
187 if self.isitem: |
|
188 self.w(self._start_li()) |
|
189 ident = self.ident |
|
190 self.w(u'<a href="%s" class="%s">%s</a>' % ( |
|
191 toggle_action(ident), self.link_class, self.label)) |
|
192 self._begin_menu(ident) |
|
193 for item in self.items: |
|
194 _bwcompatible_render_item(self.w, item) |
|
195 self._end_menu() |
|
196 if self.isitem: |
|
197 self.w(u'</li>') |
|
198 |
|
199 |
|
200 class PopupBoxMenu(BoxMenu): |
|
201 """like BoxMenu but uses div and specific css class |
|
202 in order to behave like a popup menu |
|
203 """ |
|
204 link_class = 'popupMenu' |
|
205 |
|
206 def _begin_menu(self, ident): |
|
207 self.w(u'<div class="popupWrapper"><div id="%s" class="hidden popup"><ul>' % ident) |
|
208 |
|
209 def _end_menu(self): |
|
210 self.w(u'</ul></div></div>') |
|
211 |
|
212 |
|
213 @add_metaclass(class_deprecated) |
|
214 class BoxField(HTMLWidget): |
|
215 """couples label / value meant to be displayed in a box""" |
|
216 __deprecation_warning__ = '[3.10] class %(cls)s is deprecated' |
|
217 def __init__(self, label, value): |
|
218 self.label = label |
|
219 self.value = value |
|
220 |
|
221 def _render(self): |
|
222 self.w(u'<li><div><span class="label">%s</span> ' |
|
223 u'<span class="value">%s</span></div></li>' |
|
224 % (self.label, self.value)) |
|
225 |
|
226 |
|
227 @add_metaclass(class_deprecated) |
|
228 class BoxSeparator(HTMLWidget): |
|
229 """a menu separator""" |
|
230 __deprecation_warning__ = '[3.10] class %(cls)s is deprecated' |
|
231 |
|
232 def _render(self): |
|
233 self.w(u'</ul><hr class="boxSeparator"/><ul>') |
|
234 |
|
235 |
|
236 @add_metaclass(class_deprecated) |
|
237 class BoxLink(HTMLWidget): |
|
238 """a link in a box""" |
|
239 __deprecation_warning__ = '[3.10] class %(cls)s is deprecated' |
|
240 def __init__(self, href, label, _class='', title='', ident='', escape=False): |
|
241 self.href = href |
|
242 if escape: |
|
243 self.label = xml_escape(label) |
|
244 else: |
|
245 self.label = label |
|
246 self._class = _class or '' |
|
247 self.title = title |
|
248 self.ident = ident |
|
249 |
|
250 def _render(self): |
|
251 link = u'<a href="%s" title="%s">%s</a>' % ( |
|
252 xml_escape(self.href), xml_escape(self.title), self.label) |
|
253 if self.ident: |
|
254 self.w(u'<li id="%s" class="%s">%s</li>\n' % (self.ident, self._class, link)) |
|
255 else: |
|
256 self.w(u'<li class="%s">%s</li>\n' % (self._class, link)) |
|
257 |
|
258 |
|
259 @add_metaclass(class_deprecated) |
|
260 class BoxHtml(HTMLWidget): |
|
261 """a form in a box""" |
|
262 __deprecation_warning__ = '[3.10] class %(cls)s is deprecated' |
|
263 def __init__(self, rawhtml): |
|
264 self.rawhtml = rawhtml |
|
265 |
|
266 def _render(self): |
|
267 self.w(self.rawhtml) |
|
268 |
|
269 |
|
270 class TableColumn(object): |
|
271 def __init__(self, name, rset_sortcol): |
|
272 """ |
|
273 :param name: the column's name |
|
274 :param rset_sortcol: the model's column used to sort this column view |
|
275 """ |
|
276 self.name = name |
|
277 self.cellrenderers = [] |
|
278 self.rset_sortcol = rset_sortcol |
|
279 self.cell_attrs = {} |
|
280 |
|
281 def append_renderer(self, cellvid, colindex): |
|
282 # XXX (adim) : why do we need colindex here ? |
|
283 self.cellrenderers.append( (cellvid, colindex) ) |
|
284 |
|
285 def add_attr(self, attr, value): |
|
286 self.cell_attrs[attr] = value |
|
287 |
|
288 |
|
289 class SimpleTableModel(object): |
|
290 """ |
|
291 uses a list of lists as a storage backend |
|
292 |
|
293 NB: the model expectes the cellvid passed to |
|
294 TableColumn.append_renderer to be a callable accepting a single |
|
295 argument and returning a unicode object |
|
296 """ |
|
297 def __init__(self, rows): |
|
298 self._rows = rows |
|
299 |
|
300 def get_rows(self): |
|
301 return self._rows |
|
302 |
|
303 def render_cell(self, cellvid, rowindex, colindex, w): |
|
304 value = self._rows[rowindex][colindex] |
|
305 w(cellvid(value)) |
|
306 |
|
307 @htmlescape |
|
308 @jsonize |
|
309 def sortvalue(self, rowindex, colindex): |
|
310 value = self._rows[rowindex][colindex] |
|
311 if value is None: |
|
312 return u'' |
|
313 elif isinstance(value, int): |
|
314 return u'%09d' % value |
|
315 else: |
|
316 return unicode(value) |
|
317 |
|
318 |
|
319 class TableWidget(HTMLWidget): |
|
320 """ |
|
321 Display data in a Table with sortable column. |
|
322 |
|
323 When using remember to include the required css and js with: |
|
324 |
|
325 self._cw.add_js('jquery.tablesorter.js') |
|
326 self._cw.add_css(('cubicweb.tablesorter.css', 'cubicweb.tableview.css')) |
|
327 """ |
|
328 highlight = "onmouseover=\"$(this).addClass('highlighted');\" " \ |
|
329 "onmouseout=\"$(this).removeClass('highlighted');\"" |
|
330 |
|
331 def __init__(self, model): |
|
332 self.model = model |
|
333 self.columns = [] |
|
334 |
|
335 def append_column(self, column): |
|
336 """ |
|
337 :type column: TableColumn |
|
338 """ |
|
339 self.columns.append(column) |
|
340 |
|
341 def _render(self): |
|
342 self.w(u'<table class="listing">') |
|
343 self.w(u'<thead>') |
|
344 self.w(u'<tr class="header">') |
|
345 for column in self.columns: |
|
346 attrs = ('%s="%s"' % (name, value) for name, value in column.cell_attrs.items()) |
|
347 self.w(u'<th %s>%s</th>' % (' '.join(attrs), column.name or u'')) |
|
348 self.w(u'</tr>') |
|
349 self.w(u'</thead><tbody>') |
|
350 for rowindex in range(len(self.model.get_rows())): |
|
351 klass = (rowindex%2==1) and 'odd' or 'even' |
|
352 self.w(u'<tr class="%s" %s>' % (klass, self.highlight)) |
|
353 for column, sortvalue in self.itercols(rowindex): |
|
354 attrs = dict(column.cell_attrs) |
|
355 attrs["cubicweb:sortvalue"] = sortvalue |
|
356 attrs = ('%s="%s"' % (name, value) for name, value in attrs.items()) |
|
357 self.w(u'<td %s>' % (' '.join(attrs))) |
|
358 for cellvid, colindex in column.cellrenderers: |
|
359 self.model.render_cell(cellvid, rowindex, colindex, w=self.w) |
|
360 self.w(u'</td>') |
|
361 self.w(u'</tr>') |
|
362 self.w(u'</tbody>') |
|
363 self.w(u'</table>') |
|
364 |
|
365 def itercols(self, rowindex): |
|
366 for column in self.columns: |
|
367 yield column, self.model.sortvalue(rowindex, column.rset_sortcol) |