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