1 """Set of tree-building widgets, based on jQuery treeview plugin |
1 """Set of tree-building widgets, based on jQuery treeview plugin |
2 |
2 |
3 :organization: Logilab |
3 :organization: Logilab |
4 :copyright: 2008-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
4 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
6 """ |
6 """ |
7 __docformat__ = "restructuredtext en" |
7 __docformat__ = "restructuredtext en" |
8 |
8 |
|
9 from logilab.common.decorators import monkeypatch |
9 from logilab.mtconverter import html_escape |
10 from logilab.mtconverter import html_escape |
10 |
11 |
11 from cubicweb.interfaces import ITree |
12 from cubicweb.interfaces import ITree |
12 from cubicweb.selectors import implements |
13 from cubicweb.selectors import implements |
13 from cubicweb.view import EntityView |
14 from cubicweb.view import EntityView |
14 from cubicweb.utils import make_uid |
15 from cubicweb.web.views.basecontrollers import JSonController |
15 |
16 |
16 def treecookiename(treeid): |
17 def treecookiename(treeid): |
17 return str('treestate-%s' % treeid) |
18 return str('treestate-%s' % treeid) |
18 |
|
19 |
19 |
20 class TreeView(EntityView): |
20 class TreeView(EntityView): |
21 id = 'treeview' |
21 id = 'treeview' |
22 itemvid = 'treeitemview' |
22 itemvid = 'treeitemview' |
23 css_classes = 'treeview widget' |
23 css_classes = 'treeview widget' |
24 title = _('tree view') |
24 title = _('tree view') |
25 |
25 |
26 def call(self, subvid=None, treeid=None, initial_load=True): |
26 def call(self, subvid=None, treeid=None, initial_load=True): |
|
27 if subvid is None and 'subvid' in self.req.form: |
|
28 subvid = self.req.form.pop('subvid') # consume it |
27 if subvid is None: |
29 if subvid is None: |
28 if 'subvid' in self.req.form: |
30 subvid = 'oneline' |
29 subvid = self.req.form.pop('subvid') # consume it |
31 if treeid is None and 'treeid' in self.req.form: |
30 else: |
32 treeid = self.req.form.pop('treeid') |
31 subvid = 'oneline' |
33 assert treeid is not None |
32 if treeid is None: |
|
33 if 'treeid' in self.req.form: |
|
34 treeid = self.req.form.pop('treeid') |
|
35 else: |
|
36 treeid = make_uid('throw away uid') |
|
37 self.warning('Tree state won\'t be properly restored after next reload') |
|
38 if initial_load: |
34 if initial_load: |
39 self.req.add_css('jquery.treeview.css') |
35 self.req.add_css('jquery.treeview.css') |
40 self.req.add_js(('cubicweb.ajax.js', 'jquery.treeview.js')) |
36 self.req.add_js(('cubicweb.ajax.js', 'jquery.treeview.js')) |
41 self.req.html_headers.add_onload(u""" |
37 self.req.html_headers.add_onload(u""" |
42 jQuery("#tree-%s").treeview({toggle: toggleTree, prerendered: true});""" % treeid) |
38 jQuery("#tree-%s").treeview({toggle: toggleTree, prerendered: true});""" % treeid) |
43 self.w(u'<ul id="tree-%s" class="%s">' % (treeid, self.css_classes)) |
39 self.w(u'<ul id="tree-%s" class="%s">' % (treeid, self.css_classes)) |
44 for rowidx in xrange(len(self.rset)): |
40 for rowidx in xrange(len(self.rset)): |
45 self.wview(self.itemvid, self.rset, row=rowidx, col=0, |
41 self.wview(self.itemvid, self.rset, row=rowidx, col=0, |
46 vid=subvid, parentvid=self.id) |
42 vid=subvid, parentvid=self.id, treeid=treeid) |
47 self.w(u'</ul>') |
43 self.w(u'</ul>') |
48 |
44 |
49 |
45 |
50 class FileTreeView(TreeView): |
46 class FileTreeView(TreeView): |
51 """specific version of the treeview to display file trees |
47 """specific version of the treeview to display file trees |
55 title = _('file tree view') |
51 title = _('file tree view') |
56 |
52 |
57 def call(self, subvid=None, treeid=None, initial_load=True): |
53 def call(self, subvid=None, treeid=None, initial_load=True): |
58 super(FileTreeView, self).call(treeid=treeid, subvid='filetree-oneline', initial_load=initial_load) |
54 super(FileTreeView, self).call(treeid=treeid, subvid='filetree-oneline', initial_load=initial_load) |
59 |
55 |
60 |
|
61 |
|
62 class FileItemInnerView(EntityView): |
56 class FileItemInnerView(EntityView): |
63 """inner view used by the TreeItemView instead of oneline view |
57 """inner view used by the TreeItemView instead of oneline view |
64 |
58 |
65 This view adds an enclosing <span> with some specific CSS classes |
59 This view adds an enclosing <span> with some specific CSS classes |
66 around the oneline view. This is needed by the jquery treeview plugin. |
60 around the oneline view. This is needed by the jquery treeview plugin. |
68 id = 'filetree-oneline' |
62 id = 'filetree-oneline' |
69 |
63 |
70 def cell_call(self, row, col): |
64 def cell_call(self, row, col): |
71 entity = self.entity(row, col) |
65 entity = self.entity(row, col) |
72 if ITree.is_implemented_by(entity.__class__) and not entity.is_leaf(): |
66 if ITree.is_implemented_by(entity.__class__) and not entity.is_leaf(): |
73 self.w(u'<div class="folder">%s</div>' % entity.view('oneline')) |
67 self.w(u'<div class="folder">%s</div>\n' % entity.view('oneline')) |
74 else: |
68 else: |
75 # XXX define specific CSS classes according to mime types |
69 # XXX define specific CSS classes according to mime types |
76 self.w(u'<div class="file">%s</div>' % entity.view('oneline')) |
70 self.w(u'<div class="file">%s</div>\n' % entity.view('oneline')) |
77 |
71 |
78 |
72 |
79 class DefaultTreeViewItemView(EntityView): |
73 class DefaultTreeViewItemView(EntityView): |
80 """default treeitem view for entities which don't implement ITree""" |
74 """default treeitem view for entities which don't implement ITree""" |
81 id = 'treeitemview' |
75 id = 'treeitemview' |
82 |
76 |
83 def cell_call(self, row, col, vid='oneline', parentvid='treeview'): |
77 def cell_call(self, row, col, vid='oneline', parentvid='treeview', treeid=None): |
|
78 assert treeid is not None |
84 entity = self.entity(row, col) |
79 entity = self.entity(row, col) |
85 itemview = self.view(vid, self.rset, row=row, col=col) |
80 itemview = self.view(vid, self.rset, row=row, col=col) |
86 if row == len(self.rset) - 1: |
81 if row == len(self.rset) - 1: |
87 self.w(u'<li class="last">%s</li>' % itemview) |
82 self.w(u'<li class="last">%s</li>' % itemview) |
88 else: |
83 else: |
93 """specific treeitem view for entities which implement ITree |
88 """specific treeitem view for entities which implement ITree |
94 |
89 |
95 (each item should be expandable if it's not a tree leaf) |
90 (each item should be expandable if it's not a tree leaf) |
96 """ |
91 """ |
97 id = 'treeitemview' |
92 id = 'treeitemview' |
98 __select__ = implements(ITree) |
93 __select__ = EntityView.__select__ & implements(ITree) # XXX |
99 |
94 |
100 def cell_call(self, row, col, vid='oneline', parentvid='treeview'): |
95 def open_state(self, eeid, treeid): |
|
96 cookies = self.req.get_cookie() |
|
97 treestate = cookies.get(treecookiename(treeid)) |
|
98 if treestate: |
|
99 return str(eeid) in treestate.value.split(';') |
|
100 return False |
|
101 |
|
102 def cell_call(self, row, col, treeid, vid='oneline', parentvid='treeview'): |
|
103 w = self.w |
101 entity = self.entity(row, col) |
104 entity = self.entity(row, col) |
102 cssclasses = [] |
105 liclasses = [] |
103 is_leaf = False |
106 is_leaf = False |
104 if row == len(self.rset) - 1: |
107 is_last = row == len(self.rset) - 1 |
105 is_leaf = True |
108 is_open = self.open_state(entity.eid, treeid) |
106 if not hasattr(entity, 'is_leaf') or entity.is_leaf(): |
109 if not hasattr(entity, 'is_leaf') or entity.is_leaf(): |
107 if is_leaf : cssclasses.append('last') |
110 if is_last: |
108 self.w(u'<li class="%s">' % u' '.join(cssclasses)) |
111 liclasses.append('last') |
|
112 w(u'<li class="%s">' % u' '.join(liclasses)) |
109 else: |
113 else: |
110 rql = entity.children_rql() % {'x': entity.eid} |
114 rql = entity.children_rql() % {'x': entity.eid} |
111 url = html_escape(self.build_url('json', rql=rql, vid=parentvid, |
115 url = html_escape(self.build_url('json', rql=rql, vid=parentvid, |
112 pageid=self.req.pageid, |
116 pageid=self.req.pageid, |
113 subvid=vid, |
117 treeid=treeid, |
114 noautoload=True)) |
118 fname='view', |
115 cssclasses.append('expandable') |
119 subvid=vid)) |
116 divclasses = ['hitarea expandable-hitarea'] |
120 divclasses = ['hitarea'] |
117 if is_leaf : |
121 if is_open: |
118 cssclasses.append('lastExpandable') |
122 liclasses.append('collapsable') |
119 divclasses.append('lastExpandable-hitarea') |
123 divclasses.append('collapsable-hitarea') |
120 self.w(u'<li cubicweb:loadurl="%s" class="%s">' % (url, u' '.join(cssclasses))) |
124 else: |
121 self.w(u'<div class="%s"> </div>' % u' '.join(divclasses)) |
125 liclasses.append('expandable') |
|
126 divclasses.append('closed-hitarea expandable-hitarea') |
|
127 if is_last: |
|
128 if is_open: |
|
129 liclasses.append('lastCollapsable') |
|
130 divclasses.append('lastCollapsable-hitarea') |
|
131 else: |
|
132 liclasses.append('lastExpandable') |
|
133 divclasses.append('lastExpandable-hitarea') |
|
134 if is_open: |
|
135 w(u'<li class="%s">' % u' '.join(liclasses)) |
|
136 else: |
|
137 w(u'<li cubicweb:loadurl="%s" class="%s">' % (url, u' '.join(liclasses))) |
|
138 if is_leaf: |
|
139 divtail = '' |
|
140 else: |
|
141 divtail = ''' onclick="asyncRemoteExec('node_clicked', '%s', '%s')"''' % \ |
|
142 (treeid, entity.eid) |
|
143 w(u'<div class="%s"%s></div>' % (u' '.join(divclasses), divtail)) |
|
144 |
122 # add empty <ul> because jquery's treeview plugin checks for |
145 # add empty <ul> because jquery's treeview plugin checks for |
123 # sublists presence |
146 # sublists presence |
124 self.w(u'<ul class="placeholder"><li>place holder</li></ul>') |
147 if not is_open: |
|
148 w(u'<ul class="placeholder"><li>place holder</li></ul>') |
|
149 # the local node info |
125 self.wview(vid, self.rset, row=row, col=col) |
150 self.wview(vid, self.rset, row=row, col=col) |
126 self.w(u'</li>') |
151 if is_open: # recurse if needed |
|
152 self.wview(parentvid, self.req.execute(rql), treeid=treeid, initial_load=False) |
|
153 w(u'</li>') |
|
154 |
|
155 |
|
156 @monkeypatch(JSonController) |
|
157 def js_node_clicked(self, treeid, nodeeid): |
|
158 """add/remove eid in treestate cookie""" |
|
159 cookies = self.req.get_cookie() |
|
160 statename = treecookiename(treeid) |
|
161 treestate = cookies.get(statename) |
|
162 if treestate is None: |
|
163 cookies[statename] = nodeeid |
|
164 self.req.set_cookie(cookies, statename) |
|
165 else: |
|
166 marked = set(filter(None, treestate.value.split(';'))) |
|
167 if nodeeid in marked: |
|
168 marked.remove(nodeeid) |
|
169 else: |
|
170 marked.add(nodeeid) |
|
171 cookies[statename] = ';'.join(marked) |
|
172 self.req.set_cookie(cookies, statename) |