|
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 |
1 from logilab.mtconverter import html_escape |
9 from logilab.mtconverter import html_escape |
|
10 from cubicweb.interfaces import ITree |
|
11 from cubicweb.common.selectors import implement_interface, yes |
|
12 from cubicweb.common.view import EntityView |
2 |
13 |
3 from cubicweb.interfaces import ITree |
14 from cubicweb.interfaces import ITree |
4 from cubicweb.selectors import implements |
15 from cubicweb.common.selectors import implement_interface, yes |
5 from cubicweb.view import EntityView |
16 from cubicweb.common.view import EntityView |
|
17 |
|
18 def treecookiename(treeid): |
|
19 return str('treestate-%s' % treeid) |
6 |
20 |
7 class TreeView(EntityView): |
21 class TreeView(EntityView): |
8 id = 'treeview' |
22 id = 'treeview' |
|
23 accepts = ('Any',) |
9 itemvid = 'treeitemview' |
24 itemvid = 'treeitemview' |
10 css_classes = 'treeview widget' |
25 css_classes = 'treeview widget' |
11 title = _('tree view') |
26 title = _('tree view') |
12 |
27 |
13 def call(self, subvid=None): |
28 def call(self, subvid=None, treeid=None, initial_load=True): |
14 if subvid is None and 'subvid' in self.req.form: |
29 if subvid is None and 'subvid' in self.req.form: |
15 subvid = self.req.form.pop('subvid') # consume it |
30 subvid = self.req.form.pop('subvid') # consume it |
16 if subvid is None: |
31 if subvid is None: |
17 subvid = 'oneline' |
32 subvid = 'oneline' |
18 self.req.add_css('jquery.treeview.css') |
33 if treeid is None and 'treeid' in self.req.form: |
19 self.req.add_js(('cubicweb.ajax.js', 'jquery.treeview.js', 'cubicweb.widgets.js')) |
34 treeid = self.req.form.pop('treeid') |
20 # XXX noautoload is a quick hack to avoid treeview to be rebuilt |
35 assert treeid is not None |
21 # after a json query and avoid double toggling bugs. |
36 if initial_load: |
22 # Need to find a way to do that cleanly. |
37 self.req.add_css('jquery.treeview.css') |
23 if 'noautoload' in self.req.form: |
38 self.req.add_js(('cubicweb.ajax.js', 'jquery.treeview.js')) |
24 self.w(u'<ul class="%s" cubicweb:wdgtype="TreeView">' % self.css_classes) |
39 self.req.html_headers.add_onload(u""" |
25 else: |
40 jQuery("#tree-%s").treeview({toggle: toggleTree, |
26 self.w(u'<ul class="%s" cubicweb:loadtype="auto" cubicweb:wdgtype="TreeView">' |
41 prerendered: true});""" % treeid) |
27 % self.css_classes) |
42 self.w(u'<ul id="tree-%s" class="%s">' % (treeid, self.css_classes)) |
28 for rowidx in xrange(len(self.rset)): |
43 for rowidx in xrange(len(self.rset)): |
29 self.wview(self.itemvid, self.rset, row=rowidx, col=0, |
44 self.wview(self.itemvid, self.rset, row=rowidx, col=0, |
30 vid=subvid, parentvid=self.id) |
45 vid=subvid, parentvid=self.id, treeid=treeid) |
31 self.w(u'</ul>') |
46 self.w(u'</ul>') |
32 |
47 |
33 |
48 |
34 class FileTreeView(TreeView): |
49 class FileTreeView(TreeView): |
35 """specific version of the treeview to display file trees |
50 """specific version of the treeview to display file trees |
36 """ |
51 """ |
37 id = 'filetree' |
52 id = 'filetree' |
38 css_classes = 'treeview widget filetree' |
53 css_classes = 'treeview widget filetree' |
39 title = _('file tree view') |
54 title = _('file tree view') |
40 |
55 |
41 def call(self, subvid=None): |
56 def call(self, subvid=None, treeid=None, initial_load=True): |
42 super(FileTreeView, self).call(subvid='filetree-oneline') |
57 super(FileTreeView, self).call(treeid=treeid, subvid='filetree-oneline', initial_load=initial_load) |
43 |
|
44 |
|
45 |
58 |
46 class FileItemInnerView(EntityView): |
59 class FileItemInnerView(EntityView): |
47 """inner view used by the TreeItemView instead of oneline view |
60 """inner view used by the TreeItemView instead of oneline view |
48 |
61 |
49 This view adds an enclosing <span> with some specific CSS classes |
62 This view adds an enclosing <span> with some specific CSS classes |
52 id = 'filetree-oneline' |
65 id = 'filetree-oneline' |
53 |
66 |
54 def cell_call(self, row, col): |
67 def cell_call(self, row, col): |
55 entity = self.entity(row, col) |
68 entity = self.entity(row, col) |
56 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(): |
57 self.w(u'<div class="folder">%s</div>' % entity.view('oneline')) |
70 self.w(u'<div class="folder">%s</div>\n' % entity.view('oneline')) |
58 else: |
71 else: |
59 # XXX define specific CSS classes according to mime types |
72 # XXX define specific CSS classes according to mime types |
60 self.w(u'<div class="file">%s</div>' % entity.view('oneline')) |
73 self.w(u'<div class="file">%s</div>\n' % entity.view('oneline')) |
61 |
74 |
62 |
75 |
63 class DefaultTreeViewItemView(EntityView): |
76 class DefaultTreeViewItemView(EntityView): |
64 """default treeitem view for entities which don't implement ITree |
77 """default treeitem view for entities which don't implement ITree |
65 """ |
78 """ |
66 id = 'treeitemview' |
79 id = 'treeitemview' |
67 |
80 accepts = ('Any',) |
68 def cell_call(self, row, col, vid='oneline', parentvid='treeview'): |
81 |
|
82 def cell_call(self, row, col, vid='oneline', parentvid='treeview', treeid=None): |
|
83 assert treeid is not None |
69 entity = self.entity(row, col) |
84 entity = self.entity(row, col) |
70 itemview = self.view(vid, self.rset, row=row, col=col) |
85 itemview = self.view(vid, self.rset, row=row, col=col) |
71 if row == len(self.rset) - 1: |
86 if row == len(self.rset) - 1: |
72 self.w(u'<li class="last">%s</li>' % itemview) |
87 self.w(u'<li class="last">%s</li>' % itemview) |
73 else: |
88 else: |
74 self.w(u'<li>%s</li>' % itemview) |
89 self.w(u'<li>%s</li>' % itemview) |
75 |
90 |
76 |
91 |
77 class TreeViewItemView(EntityView): |
92 class TreeViewItemView(EntityView): |
78 """specific treeitem view for entities which implement ITree |
93 """specific treeitem view for entities which implement ITree |
79 |
94 |
80 (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) |
81 """ |
96 """ |
82 id = 'treeitemview' |
97 id = 'treeitemview' |
83 __select__ = implements(ITree) |
98 # XXX append yes to make sure we get an higher score than |
84 |
99 # the default treeitem view |
85 def cell_call(self, row, col, vid='oneline', parentvid='treeview'): |
100 __selectors__ = (implement_interface, yes) |
|
101 accepts_interfaces = (ITree,) |
|
102 |
|
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 |
86 entity = self.entity(row, col) |
112 entity = self.entity(row, col) |
87 cssclasses = [] |
113 liclasses = [] |
88 is_leaf = False |
114 is_leaf = False |
89 if row == len(self.rset) - 1: |
115 is_last = row == len(self.rset) - 1 |
90 is_leaf = True |
116 is_open = self.open_state(entity.eid, treeid) |
91 if not hasattr(entity, 'is_leaf') or entity.is_leaf(): |
117 if not hasattr(entity, 'is_leaf') or entity.is_leaf(): |
92 if is_leaf : cssclasses.append('last') |
118 if is_last: |
93 self.w(u'<li class="%s">' % u' '.join(cssclasses)) |
119 liclasses.append('last') |
|
120 w(u'<li class="%s">' % u' '.join(liclasses)) |
94 else: |
121 else: |
95 rql = entity.children_rql() % {'x': entity.eid} |
122 rql = entity.children_rql() % {'x': entity.eid} |
96 url = html_escape(self.build_url('json', rql=rql, vid=parentvid, |
123 url = html_escape(self.build_url('json', rql=rql, vid=parentvid, |
97 pageid=self.req.pageid, |
124 pageid=self.req.pageid, |
98 subvid=vid, |
125 treeid=treeid, |
99 noautoload=True)) |
126 subvid=vid)) |
100 cssclasses.append('expandable') |
127 divclasses = ['hitarea'] |
101 divclasses = ['hitarea expandable-hitarea'] |
128 if is_open: |
102 if is_leaf : |
129 liclasses.append('collapsable') |
103 cssclasses.append('lastExpandable') |
130 divclasses.append('collapsable-hitarea') |
104 divclasses.append('lastExpandable-hitarea') |
131 else: |
105 self.w(u'<li cubicweb:loadurl="%s" class="%s">' % (url, u' '.join(cssclasses))) |
132 liclasses.append('expandable') |
106 self.w(u'<div class="%s"> </div>' % u' '.join(divclasses)) |
133 divclasses.append('closed-hitarea expandable-hitarea') |
107 |
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 |
108 # add empty <ul> because jquery's treeview plugin checks for |
152 # add empty <ul> because jquery's treeview plugin checks for |
109 # sublists presence |
153 # sublists presence |
110 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 |
111 self.wview(vid, self.rset, row=row, col=col) |
157 self.wview(vid, self.rset, row=row, col=col) |
112 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>') |
113 |
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) |